001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.rsgroup;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertThrows;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import java.io.IOException;
026import java.io.StringWriter;
027import java.util.ArrayList;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import org.apache.hadoop.conf.Configuration;
033import org.apache.hadoop.hbase.HConstants;
034import org.apache.hadoop.hbase.LocalHBaseCluster;
035import org.apache.hadoop.hbase.ServerName;
036import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
037import org.apache.hadoop.hbase.client.Get;
038import org.apache.hadoop.hbase.client.RegionInfo;
039import org.apache.hadoop.hbase.client.Result;
040import org.apache.hadoop.hbase.client.Table;
041import org.apache.hadoop.hbase.net.Address;
042import org.apache.hadoop.hbase.testclassification.MediumTests;
043import org.apache.hadoop.hbase.testclassification.RSGroupTests;
044import org.apache.hadoop.hbase.util.Bytes;
045import org.apache.hadoop.hbase.util.DNS;
046import org.apache.hadoop.hbase.util.JVMClusterUtil;
047import org.junit.jupiter.api.AfterAll;
048import org.junit.jupiter.api.AfterEach;
049import org.junit.jupiter.api.BeforeAll;
050import org.junit.jupiter.api.BeforeEach;
051import org.junit.jupiter.api.Tag;
052import org.junit.jupiter.api.Test;
053import org.junit.jupiter.api.TestInfo;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056
057import org.apache.hbase.thirdparty.com.google.common.collect.Sets;
058
059import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
060import org.apache.hadoop.hbase.shaded.protobuf.generated.RSGroupProtos;
061
062/**
063 * Black-box tests for regex-based automatic RSGroup membership
064 * (hbase.rsgroup.regex.<groupname>=<regex>), driven entirely through the public
065 * {@link org.apache.hadoop.hbase.client.Admin} RSGroup API, live RegionServer add/remove against
066 * the minicluster, and captured log output -- no reflection or visibility changes against
067 * {@link RSGroupInfoManagerImpl}.
068 */
069@Tag(RSGroupTests.TAG)
070@Tag(MediumTests.TAG)
071public class TestRegexBasedRSGroupMembership extends TestRSGroupsBase {
072
073  private static final Logger LOG = LoggerFactory.getLogger(TestRegexBasedRSGroupMembership.class);
074  private static final String REGEX_PREFIX = "hbase.rsgroup.regex.";
075
076  private final List<JVMClusterUtil.RegionServerThread> fakeRegionServers = new ArrayList<>();
077  private final Set<String> regexConfigKeysSet = new HashSet<>();
078
079  @BeforeAll
080  public static void setUp() throws Exception {
081    setUpTestBeforeClass();
082  }
083
084  @AfterAll
085  public static void tearDown() throws Exception {
086    tearDownAfterClass();
087  }
088
089  @BeforeEach
090  public void beforeMethod(TestInfo testInfo) throws Exception {
091    setUpBeforeMethod(testInfo);
092  }
093
094  @AfterEach
095  public void afterMethod() throws Exception {
096    // Stop any fake-hostname RS this test started before handing back to the shared teardown,
097    // which only ever starts servers to top back up to NUM_SLAVES_BASE, never stops extras.
098    for (JVMClusterUtil.RegionServerThread rst : new ArrayList<>(fakeRegionServers)) {
099      stopFakeRegionServer(rst);
100    }
101    // The MASTER/ADMIN Configuration objects are shared across every test method in this class;
102    // undo any regex config this test added to both so it can't leak into the next method.
103    for (String key : regexConfigKeysSet) {
104      MASTER.getConfiguration().unset(key);
105      ADMIN.getConfiguration().unset(key);
106    }
107    regexConfigKeysSet.clear();
108    tearDownAfterMethod();
109    RSGroupBasedLoadBalancer balancer = (RSGroupBasedLoadBalancer) MASTER.getLoadBalancer();
110    balancer.resetAssignmentCallFlagsForTest();
111    balancer.setFallbackEnabledForTest(false);
112  }
113
114  // ============================== helpers ==============================
115
116  /**
117   * Starts a genuinely new RegionServer process (thread) in the shared minicluster that reports
118   * itself under a caller-chosen hostname, so regex membership rules can target it individually.
119   * The base cluster's real RS all share one real, resolvable hostname (this machine's address);
120   * the RPC services constructor unconditionally resolves whatever hostname is configured
121   * (independent of any RPC bind-address override), so the caller-chosen hostname here must itself
122   * be a real, resolvable, bindable literal -- "127.0.0.1" and "localhost" both work and are
123   * guaranteed distinct from the base cluster's real hostname, which is exactly what every test in
124   * this class relies on to get a controllable, non-shared identity.
125   */
126  private JVMClusterUtil.RegionServerThread startFakeHostnameRS(String hostname) throws Exception {
127    Configuration rsConf = new Configuration(TEST_UTIL.getConfiguration());
128    rsConf.set(DNS.UNSAFE_RS_HOSTNAME_KEY, hostname);
129    rsConf.set("hbase.regionserver.ipc.address", "localhost");
130    // The shared TEST_UTIL configuration's regionserver port is never rewritten to an ephemeral
131    // value by minicluster startup (the base RS get their distinct ports through a separate,
132    // per-instance mechanism), so without this override every fake RS started here would try to
133    // bind the literal default port (16020) and collide with any other fake RS still alive.
134    rsConf.set(HConstants.REGIONSERVER_PORT, "0");
135    // Same issue for the embedded web UI (default port 16030); these fake RS don't need one.
136    rsConf.set(HConstants.REGIONSERVER_INFO_PORT, "-1");
137    LocalHBaseCluster localCluster = ((SingleProcessHBaseCluster) CLUSTER).hbaseCluster;
138    JVMClusterUtil.RegionServerThread rst =
139      localCluster.addRegionServer(rsConf, localCluster.getRegionServers().size());
140    rst.start();
141    rst.waitForServerOnline();
142    fakeRegionServers.add(rst);
143    final ServerName sn = rst.getRegionServer().getServerName();
144    TEST_UTIL.waitFor(WAIT_TIMEOUT,
145      () -> MASTER.getServerManager().getOnlineServersList().contains(sn));
146    LOG.info("Started fake-hostname RS {}", sn);
147    return rst;
148  }
149
150  private void stopFakeRegionServer(JVMClusterUtil.RegionServerThread rst) throws Exception {
151    fakeRegionServers.remove(rst);
152    final ServerName sn = rst.getRegionServer().getServerName();
153    rst.getRegionServer().stop("test cleanup");
154    TEST_UTIL.waitFor(WAIT_TIMEOUT,
155      () -> !MASTER.getServerManager().getOnlineServersList().contains(sn));
156    LOG.info("Stopped fake-hostname RS {}", sn);
157  }
158
159  private void killFakeRegionServer(JVMClusterUtil.RegionServerThread rst) throws Exception {
160    fakeRegionServers.remove(rst);
161    final ServerName sn = rst.getRegionServer().getServerName();
162    ((SingleProcessHBaseCluster) CLUSTER).killRegionServer(sn);
163    TEST_UTIL.waitFor(WAIT_TIMEOUT,
164      () -> !MASTER.getServerManager().getOnlineServersList().contains(sn));
165    LOG.info("Killed fake-hostname RS {}", sn);
166  }
167
168  /**
169   * Server-membership recompute happens synchronously in the {@code serverAdded}/
170   * {@code serverRemoved} callback -- it is only ever triggered by an actual server add/remove
171   * event, a bare config mutation is not enough. Forces such an event (add+remove a bystander RS)
172   * so a config change just made takes effect without requiring an explicit RSGroup admin RPC. Only
173   * used by tests that *want* the automatic reconciliation to run and correct things; the trigger
174   * RS's own transient membership doesn't affect assertions elsewhere since those only check for
175   * specific other servers' addresses.
176   */
177  private void triggerListenerCycle() throws Exception {
178    JVMClusterUtil.RegionServerThread trigger = startFakeHostnameRS("127.0.0.1");
179    stopFakeRegionServer(trigger);
180  }
181
182  /** Reads the server set of a group straight from its row in the hbase:rsgroup table. */
183  private Set<Address> readStoredServers(String groupName) throws IOException {
184    try (
185      Table table = TEST_UTIL.getConnection().getTable(RSGroupInfoManagerImpl.RSGROUP_TABLE_NAME)) {
186      Result result = table.get(new Get(Bytes.toBytes(groupName)));
187      byte[] value = result.getValue(RSGroupInfoManagerImpl.META_FAMILY_BYTES,
188        RSGroupInfoManagerImpl.META_QUALIFIER_BYTES);
189      if (value == null) {
190        return new HashSet<>();
191      }
192      return new HashSet<>(
193        ProtobufUtil.toGroupInfo(RSGroupProtos.RSGroupInfo.parseFrom(value)).getServers());
194    }
195  }
196
197  /**
198   * Fails over to a freshly started master (the minicluster's masters are threads): a backup is
199   * started, the active master is stopped, and the shared MASTER/ADMIN handles are re-pointed. The
200   * base class keeps the master's embedded RS in a "master" group, whose stored address would go
201   * stale, so the master is first moved back to 'default' (whose membership is live-computed); the
202   * next cleanup places the new master in the "master" group again.
203   */
204  private void restartMaster() throws Exception {
205    SingleProcessHBaseCluster cluster = TEST_UTIL.getMiniHBaseCluster();
206    int oldIndex = -1;
207    List<JVMClusterUtil.MasterThread> masters = cluster.getMasterThreads();
208    for (int i = 0; i < masters.size(); i++) {
209      if (masters.get(i).getMaster() == MASTER) {
210        oldIndex = i;
211      }
212    }
213    cluster.startMaster();
214    cluster.stopMaster(oldIndex);
215    cluster.waitOnMaster(oldIndex);
216    assertTrue(cluster.waitForActiveAndReadyMaster(WAIT_TIMEOUT));
217    // Clients bootstrap from the configured master address, which is now the dead master's.
218    TEST_UTIL.getConfiguration().set(HConstants.MASTER_ADDRS_KEY,
219      cluster.getMaster().getServerName().getAddress().toString());
220    TEST_UTIL.closeConnection();
221    ADMIN.close();
222    initialize();
223  }
224
225  private void setRegex(String groupName, String regex) {
226    String key = REGEX_PREFIX + groupName;
227    MASTER.getConfiguration().set(key, regex);
228    // ADMIN's own Configuration is a distinct object from MASTER's (the minicluster deep-copies
229    // configuration per daemon), so mirror the change there too -- VerifyingRSGroupAdmin#verify
230    // reads it to know which servers' membership is regex-driven.
231    ADMIN.getConfiguration().set(key, regex);
232    regexConfigKeysSet.add(key);
233  }
234
235  /**
236   * Tears down a regex-governed group. Explicit moves that contradict an active regex are rejected,
237   * so the regex is first dropped on the master only -- ADMIN's copy still tells
238   * VerifyingRSGroupAdmin which servers' membership is regex-driven -- and then the group is
239   * emptied and removed.
240   */
241  private void removeRegexGroup(String groupName) throws Exception {
242    String key = REGEX_PREFIX + groupName;
243    MASTER.getConfiguration().unset(key);
244    try {
245      removeGroup(groupName);
246    } finally {
247      clearRegex(groupName);
248    }
249  }
250
251  private void clearRegex(String groupName) {
252    String key = REGEX_PREFIX + groupName;
253    MASTER.getConfiguration().unset(key);
254    ADMIN.getConfiguration().unset(key);
255    regexConfigKeysSet.remove(key);
256  }
257
258  private Address addressOf(JVMClusterUtil.RegionServerThread rst) {
259    return rst.getRegionServer().getServerName().getAddress();
260  }
261
262  private static class LogCapturer {
263    private final StringWriter sw = new StringWriter();
264    private final org.apache.logging.log4j.core.appender.WriterAppender appender;
265    private final org.apache.logging.log4j.core.Logger logger;
266
267    LogCapturer(org.apache.logging.log4j.core.Logger logger) {
268      this.logger = logger;
269      this.appender = org.apache.logging.log4j.core.appender.WriterAppender.newBuilder()
270        .setName("test-regex-membership").setTarget(sw).build();
271      this.appender.start();
272      this.logger.addAppender(this.appender);
273    }
274
275    String getOutput() {
276      return sw.toString();
277    }
278
279    void stopCapturing() {
280      this.logger.removeAppender(this.appender);
281      this.appender.stop();
282    }
283  }
284
285  private static LogCapturer captureRSGroupInfoManagerLog() {
286    return new LogCapturer(
287      (org.apache.logging.log4j.core.Logger) org.apache.logging.log4j.LogManager
288        .getLogger(RSGroupInfoManagerImpl.class));
289  }
290
291  // ============================== tests ==============================
292
293  @Test
294  public void testNoRegexConfigBaseline() throws Exception {
295    // No hbase.rsgroup.regex.* configured at all: every server must remain in default, exactly
296    // as it would without this feature. This is also implicitly exercised by every other test in
297    // this module, but pin it down explicitly for this feature.
298    RSGroupInfo defaultGroup = ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP);
299    assertEquals(NUM_SLAVES_BASE, defaultGroup.getServers().size());
300  }
301
302  @Test
303  public void testSingleRegexAutoJoinOnLiveAdd() throws Exception {
304    String groupName = getGroupName("auto");
305    ADMIN.addRSGroup(groupName);
306    setRegex(groupName, "127\\.0\\.0\\.1");
307
308    JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
309    Address addr = addressOf(rst);
310    try {
311      TEST_UTIL.waitFor(WAIT_TIMEOUT,
312        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr));
313      // No explicit moveServersToRSGroup call was made -- the server auto-joined purely from the
314      // ServerListener reacting to its own arrival.
315      assertFalse(ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
316    } finally {
317      removeRegexGroup(groupName);
318    }
319  }
320
321  @Test
322  public void testRegexWithQuantifiersMatches() throws Exception {
323    String groupName = getGroupName("quant");
324    ADMIN.addRSGroup(groupName);
325    setRegex(groupName, "127\\.0\\.0\\.\\d{1,3}");
326
327    JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
328    Address addr = addressOf(rst);
329    try {
330      TEST_UTIL.waitFor(WAIT_TIMEOUT,
331        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr));
332      // No explicit moveServersToRSGroup call was made -- the server auto-joined purely from the
333      // ServerListener reacting to its own arrival.
334      assertFalse(ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
335    } finally {
336      removeRegexGroup(groupName);
337    }
338  }
339
340  @Test
341  public void testFullStringMatchSemantics() throws Exception {
342    // The regex is a strict prefix of the hostname; full-string Pattern#matches semantics must
343    // NOT treat this as a match.
344    String groupName = getGroupName("prefix");
345    ADMIN.addRSGroup(groupName);
346    // Strict prefix of "127.0.0.1" -- must NOT match under full-string Pattern#matches semantics.
347    setRegex(groupName, "127\\.0\\.0");
348
349    JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
350    Address addr = addressOf(rst);
351    try {
352      TEST_UTIL.waitFor(WAIT_TIMEOUT,
353        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
354      assertFalse(ADMIN.getRSGroup(groupName).getServers().contains(addr));
355    } finally {
356      removeRegexGroup(groupName);
357    }
358  }
359
360  @Test
361  public void testRegexTargetingDefaultGroupIsNoOpAndWarns() throws Exception {
362    LogCapturer capturer = captureRSGroupInfoManagerLog();
363    try {
364      setRegex(RSGroupInfo.DEFAULT_GROUP, "127\\.0\\.0\\..*");
365      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
366      Address addr = addressOf(rst);
367      TEST_UTIL.waitFor(WAIT_TIMEOUT,
368        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
369      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> capturer.getOutput()
370        .contains("cannot target the reserved '" + RSGroupInfo.DEFAULT_GROUP + "' group"));
371    } finally {
372      capturer.stopCapturing();
373      clearRegex(RSGroupInfo.DEFAULT_GROUP);
374    }
375  }
376
377  @Test
378  public void testInvalidRegexSyntaxIgnoredAndWarns() throws Exception {
379    String groupName = getGroupName("badregex");
380    ADMIN.addRSGroup(groupName);
381    LogCapturer capturer = captureRSGroupInfoManagerLog();
382    try {
383      setRegex(groupName, "[");
384      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
385      Address addr = addressOf(rst);
386      TEST_UTIL.waitFor(WAIT_TIMEOUT,
387        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
388      TEST_UTIL.waitFor(WAIT_TIMEOUT,
389        () -> capturer.getOutput().contains("Invalid regex '[' for RSGroup '" + groupName + "'"));
390    } finally {
391      capturer.stopCapturing();
392      removeRegexGroup(groupName);
393    }
394  }
395
396  @Test
397  public void testInvalidGroupNameKeyIgnoredAndWarns() throws Exception {
398    LogCapturer capturer = captureRSGroupInfoManagerLog();
399    try {
400      setRegex("not a valid name", "127\\.0\\.0\\..*");
401      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
402      Address addr = addressOf(rst);
403      TEST_UTIL.waitFor(WAIT_TIMEOUT,
404        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
405      TEST_UTIL.waitFor(WAIT_TIMEOUT,
406        () -> capturer.getOutput().contains("is not a valid RSGroup name"));
407    } finally {
408      capturer.stopCapturing();
409      clearRegex("not a valid name");
410    }
411  }
412
413  @Test
414  public void testListenerDoesNotStickToStaleServer() throws Exception {
415    // A server leaves, then a *different* server with a matching hostname joins later; the
416    // recompute must not get stuck comparing against a stale previous-assignments snapshot.
417    String groupName = getGroupName("stale");
418    ADMIN.addRSGroup(groupName);
419    setRegex(groupName, "127\\.0\\.0\\..*");
420    try {
421      JVMClusterUtil.RegionServerThread first = startFakeHostnameRS("127.0.0.1");
422      Address firstAddr = addressOf(first);
423      TEST_UTIL.waitFor(WAIT_TIMEOUT,
424        () -> ADMIN.getRSGroup(groupName).getServers().contains(firstAddr));
425
426      stopFakeRegionServer(first);
427
428      // Different startcode (fresh ServerName) under the same matching hostname.
429      JVMClusterUtil.RegionServerThread second = startFakeHostnameRS("127.0.0.1");
430      Address secondAddr = addressOf(second);
431      TEST_UTIL.waitFor(WAIT_TIMEOUT,
432        () -> ADMIN.getRSGroup(groupName).getServers().contains(secondAddr));
433    } finally {
434      removeRegexGroup(groupName);
435    }
436  }
437
438  @Test
439  public void testDanglingGroupFallsBackThenResolvesOnceGroupCreated() throws Exception {
440    String groupName = getGroupName("dangling");
441    LogCapturer capturer = captureRSGroupInfoManagerLog();
442    try {
443      // Group does not exist yet when the regex is configured and the matching RS joins.
444      setRegex(groupName, "127\\.0\\.0\\..*");
445      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
446      Address addr = addressOf(rst);
447      TEST_UTIL.waitFor(WAIT_TIMEOUT,
448        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
449      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> capturer.getOutput()
450        .contains("RSGroup '" + groupName + "' does not exist -- create it with addRSGroup"));
451
452      // Now create the group; on the next cycle the server should migrate in.
453      ADMIN.addRSGroup(groupName);
454      triggerListenerCycle();
455      TEST_UTIL.waitFor(WAIT_TIMEOUT,
456        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr));
457    } finally {
458      capturer.stopCapturing();
459      removeRegexGroup(groupName);
460    }
461  }
462
463  @Test
464  public void testAmbiguousMatchFallsBackThenResolvesOnceOverlapFixed() throws Exception {
465    String groupOne = getGroupName("ambig1");
466    String groupTwo = getGroupName("ambig2");
467    ADMIN.addRSGroup(groupOne);
468    ADMIN.addRSGroup(groupTwo);
469    LogCapturer capturer = captureRSGroupInfoManagerLog();
470    try {
471      setRegex(groupOne, "127\\.0\\.0\\..*");
472      setRegex(groupTwo, "127\\.0\\.0\\.1");
473
474      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
475      Address addr = addressOf(rst);
476      TEST_UTIL.waitFor(WAIT_TIMEOUT,
477        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
478      TEST_UTIL.waitFor(WAIT_TIMEOUT,
479        () -> capturer.getOutput().contains("matches regexes for multiple RSGroups"));
480      assertFalse(ADMIN.getRSGroup(groupOne).getServers().contains(addr));
481      assertFalse(ADMIN.getRSGroup(groupTwo).getServers().contains(addr));
482
483      // Fix the overlap
484      setRegex(groupOne, "10\\.0\\.0\\..*");
485      triggerListenerCycle();
486      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> ADMIN.getRSGroup(groupTwo).getServers().contains(addr));
487    } finally {
488      capturer.stopCapturing();
489      removeRegexGroup(groupOne);
490      removeRegexGroup(groupTwo);
491    }
492  }
493
494  @Test
495  public void testRegexCoveringEveryServerEmptiesDefaultAndWarns() throws Exception {
496    // Regex placement is always applied: a catch-all regex matches every online server (all base
497    // RS share one real hostname), so they all move into the regex group and default is left
498    // empty. That is a warning about an unusual configuration, not an error, and fallback is what
499    // lets default's tables keep being assigned in that state.
500    // (Explicit moveServersToRSGroup can never empty default -- an unrelated, longstanding
501    // RSGroupInfoManagerImpl#moveServers guard keeps >=1 server there -- so the empty-default
502    // state is driven purely through the automatic regex-reconciliation path.)
503    String regexGroup = getGroupName("emptydefault");
504    ADMIN.addRSGroup(regexGroup);
505    RSGroupBasedLoadBalancer balancer = (RSGroupBasedLoadBalancer) MASTER.getLoadBalancer();
506    balancer.setFallbackEnabledForTest(true);
507    LogCapturer capturer = captureRSGroupInfoManagerLog();
508    try {
509      setRegex(regexGroup, ".*");
510      triggerListenerCycle();
511
512      TEST_UTIL.waitFor(WAIT_TIMEOUT,
513        () -> capturer.getOutput().contains("leaves RSGroup 'default' with no online servers"));
514      assertTrue(capturer.getOutput().contains(RSGroupBasedLoadBalancer.FALLBACK_GROUP_ENABLE_KEY));
515      TEST_UTIL.waitFor(WAIT_TIMEOUT,
516        () -> ADMIN.getRSGroup(regexGroup).getServers().size() == NUM_SLAVES_BASE);
517      assertTrue(ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().isEmpty());
518
519      // With default empty and fallback on, a table in default is still assigned -- onto servers
520      // of other groups.
521      TEST_UTIL.createMultiRegionTable(tableName, Bytes.toBytes("f"), 5);
522      TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
523      assertEquals(RSGroupInfo.DEFAULT_GROUP, ADMIN.getRSGroup(tableName).getName());
524
525      // Narrow the regex so it only targets a new server, not the base cluster's shared
526      // hostname -- the base servers are unmatched again and return to default.
527      setRegex(regexGroup, "127\\.0\\.0\\..*");
528      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
529      Address addr = addressOf(rst);
530      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> ADMIN.getRSGroup(regexGroup).getServers().contains(addr)
531        && ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().size() == NUM_SLAVES_BASE);
532    } finally {
533      capturer.stopCapturing();
534      removeRegexGroup(regexGroup);
535    }
536  }
537
538  @Test
539  public void testCatchAllRegexMovesEveryServerAndGroupTablesStayAssigned() throws Exception {
540    // A regex group with a live member and a bound table is broadened to a catch-all. Every
541    // server is placed in its regex group (default is left empty), and the group's table stays
542    // fully assigned on its original member.
543    String groupName = getGroupName("catchall");
544    ADMIN.addRSGroup(groupName);
545    setRegex(groupName, "127\\.0\\.0\\.1");
546    RSGroupBasedLoadBalancer balancer = (RSGroupBasedLoadBalancer) MASTER.getLoadBalancer();
547    balancer.setFallbackEnabledForTest(true);
548    try {
549      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
550      Address addr = addressOf(rst);
551      TEST_UTIL.waitFor(WAIT_TIMEOUT,
552        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr));
553      ServerName sn = getServerName(addr);
554
555      TEST_UTIL.createMultiRegionTable(tableName, Bytes.toBytes("f"), 5);
556      TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
557      ADMIN.setRSGroup(Sets.newHashSet(tableName), groupName);
558      assertEquals(5, getTableServerRegionMap().get(tableName).get(sn).size());
559
560      setRegex(groupName, ".*");
561      triggerListenerCycle();
562      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> {
563        Set<Address> servers = ADMIN.getRSGroup(groupName).getServers();
564        return servers.contains(addr) && servers.size() == NUM_SLAVES_BASE + 1
565          && ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().isEmpty();
566      });
567      assertEquals(5, getTableServerRegionMap().get(tableName).get(sn).size());
568    } finally {
569      removeRegexGroup(groupName);
570    }
571  }
572
573  @Test
574  public void testRegexGovernedGroupMembershipIsNotPersisted() throws Exception {
575    // Regex-governed membership is computed on the fly: it is visible even while hbase:rsgroup is
576    // unavailable, and neither the table row nor any later flush ever carries it.
577    String groupName = getGroupName("nopersist");
578    ADMIN.addRSGroup(groupName);
579    setRegex(groupName, "127\\.0\\.0\\.1");
580    boolean tableDisabled = false;
581    try {
582      TEST_UTIL.getAdmin().disableTable(RSGroupInfoManagerImpl.RSGROUP_TABLE_NAME);
583      tableDisabled = true;
584
585      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
586      Address addr = addressOf(rst);
587      TEST_UTIL.waitFor(WAIT_TIMEOUT,
588        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr));
589
590      TEST_UTIL.getAdmin().enableTable(RSGroupInfoManagerImpl.RSGROUP_TABLE_NAME);
591      tableDisabled = false;
592      // Any group mutation flushes every group.
593      String flushGroup = getGroupName("nopersistflush");
594      ADMIN.addRSGroup(flushGroup);
595      removeGroup(flushGroup);
596
597      assertTrue(ADMIN.getRSGroup(groupName).getServers().contains(addr));
598      assertTrue(readStoredServers(groupName).isEmpty());
599    } finally {
600      if (tableDisabled) {
601        TEST_UTIL.getAdmin().enableTable(RSGroupInfoManagerImpl.RSGROUP_TABLE_NAME);
602      }
603      removeRegexGroup(groupName);
604    }
605  }
606
607  @Test
608  public void testMoveContradictingRegexIsRejected() throws Exception {
609    String groupName = getGroupName("reject");
610    String otherGroup = getGroupName("rejectother");
611    ADMIN.addRSGroup(groupName);
612    ADMIN.addRSGroup(otherGroup);
613    setRegex(groupName, "127\\.0\\.0\\..*");
614    try {
615      // A server whose hostname does not match the regex cannot be moved into the regex group ...
616      JVMClusterUtil.RegionServerThread nonMatching = startFakeHostnameRS("localhost");
617      Address nonMatchingAddr = addressOf(nonMatching);
618      TEST_UTIL.waitFor(WAIT_TIMEOUT,
619        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(nonMatchingAddr));
620      assertThrows(IOException.class,
621        () -> ADMIN.moveServersToRSGroup(Sets.newHashSet(nonMatchingAddr), groupName));
622      assertTrue(
623        ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(nonMatchingAddr));
624      assertFalse(ADMIN.getRSGroup(groupName).getServers().contains(nonMatchingAddr));
625
626      // ... but can still be placed in any other group.
627      ADMIN.moveServersToRSGroup(Sets.newHashSet(nonMatchingAddr), otherGroup);
628      assertTrue(ADMIN.getRSGroup(otherGroup).getServers().contains(nonMatchingAddr));
629      ADMIN.moveServersToRSGroup(Sets.newHashSet(nonMatchingAddr), RSGroupInfo.DEFAULT_GROUP);
630
631      // A server the regex claims cannot be moved out of, or to any group other than, its group.
632      JVMClusterUtil.RegionServerThread matching = startFakeHostnameRS("127.0.0.1");
633      Address matchingAddr = addressOf(matching);
634      TEST_UTIL.waitFor(WAIT_TIMEOUT,
635        () -> ADMIN.getRSGroup(groupName).getServers().contains(matchingAddr));
636      assertThrows(IOException.class,
637        () -> ADMIN.moveServersToRSGroup(Sets.newHashSet(matchingAddr), RSGroupInfo.DEFAULT_GROUP));
638      assertThrows(IOException.class,
639        () -> ADMIN.moveServersToRSGroup(Sets.newHashSet(matchingAddr), otherGroup));
640      assertTrue(ADMIN.getRSGroup(groupName).getServers().contains(matchingAddr));
641    } finally {
642      removeGroup(otherGroup);
643      removeRegexGroup(groupName);
644    }
645  }
646
647  @Test
648  public void testUnrelatedAdminOpsSucceedUnderActiveRegex() throws Exception {
649    String regexGroup = getGroupName("activeregex");
650    String otherGroup = getGroupName("other");
651    ADMIN.addRSGroup(regexGroup);
652    setRegex(regexGroup, "127\\.0\\.0\\..*");
653    try {
654      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
655      Address addr = addressOf(rst);
656      TEST_UTIL.waitFor(WAIT_TIMEOUT,
657        () -> ADMIN.getRSGroup(regexGroup).getServers().contains(addr));
658
659      // Admin operations on admin-managed groups must keep working while a regex-governed group is
660      // active. setRSGroup requires the target group to already have at least one server, so use a
661      // base (non-regex-matched, admin-manageable) server rather than an empty new group.
662      RSGroupInfo otherGroupInfo = addGroup(otherGroup, 1);
663      Set<Address> otherGroupServers = otherGroupInfo.getServers();
664      TEST_UTIL.createTable(tableName, Bytes.toBytes("f"));
665      ADMIN.setRSGroup(Sets.newHashSet(tableName), otherGroup);
666      // Table-to-group membership lives on the TableDescriptor (RegionServerGroup), not in
667      // RSGroupInfo#getTables() -- the latter is only ever populated by flushConfig/refresh from
668      // persisted storage and setRSGroup never touches it. ADMIN.getRSGroup(TableName) is the
669      // live, authoritative direction to check this (see RSGroupUtil#getRSGroupInfo).
670      assertEquals(otherGroup, ADMIN.getRSGroup(tableName).getName());
671
672      String renamedGroup = otherGroup + "_renamed";
673      ADMIN.renameRSGroup(otherGroup, renamedGroup);
674      RSGroupInfo renamedGroupInfo = ADMIN.getRSGroup(renamedGroup);
675      assertEquals(otherGroupServers, renamedGroupInfo.getServers());
676      assertEquals(renamedGroup, ADMIN.getRSGroup(tableName).getName());
677      assertFalse(ADMIN.listRSGroups().stream().anyMatch(g -> g.getName().equals(otherGroup)));
678
679      ADMIN.setRSGroup(Sets.newHashSet(tableName), RSGroupInfo.DEFAULT_GROUP);
680      assertEquals(RSGroupInfo.DEFAULT_GROUP, ADMIN.getRSGroup(tableName).getName());
681
682      ADMIN.moveServersToRSGroup(ADMIN.getRSGroup(renamedGroup).getServers(),
683        RSGroupInfo.DEFAULT_GROUP);
684      Set<Address> defaultServersAfterMoveBack =
685        ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers();
686      for (Address server : otherGroupServers) {
687        assertTrue(defaultServersAfterMoveBack.contains(server));
688      }
689      assertTrue(ADMIN.getRSGroup(renamedGroup).getServers().isEmpty());
690
691      ADMIN.removeRSGroup(renamedGroup);
692      assertFalse(ADMIN.listRSGroups().stream().anyMatch(g -> g.getName().equals(renamedGroup)));
693
694      // Confirm the unrelated operations above never disturbed the active regex-governed group.
695      assertTrue(ADMIN.getRSGroup(regexGroup).getServers().contains(addr));
696    } finally {
697      TEST_UTIL.deleteTable(tableName);
698      removeRegexGroup(regexGroup);
699    }
700  }
701
702  @Test
703  public void testAutomaticMoveDoesNotFireCoprocessorHooks() throws Exception {
704    String groupName = getGroupName("hooks");
705    ADMIN.addRSGroup(groupName);
706    setRegex(groupName, "127\\.0\\.0\\..*");
707    try {
708      assertFalse(OBSERVER.preMoveServersCalled);
709      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
710      Address addr = addressOf(rst);
711      TEST_UTIL.waitFor(WAIT_TIMEOUT,
712        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr));
713      // Purely automatic, event-driven reassignment must not go through the MasterRpcServices
714      // moveServers RPC path, so the moveServers coprocessor hooks must not fire.
715      assertFalse(OBSERVER.preMoveServersCalled);
716      assertFalse(OBSERVER.postMoveServersCalled);
717
718      // An explicit admin-driven move, by contrast, does fire the hooks. Use one of the
719      // base cluster's own (non-regex-matched) RS for this, rather than disturbing the
720      // regex-matched fake RS started above, which keeps running under its active regex.
721      String otherGroup = getGroupName("hooksother");
722      ADMIN.addRSGroup(otherGroup);
723      Address baseServerAddr =
724        ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().iterator().next();
725      ADMIN.moveServersToRSGroup(Sets.newHashSet(baseServerAddr), otherGroup);
726      assertTrue(OBSERVER.preMoveServersCalled);
727      assertTrue(OBSERVER.postMoveServersCalled);
728      removeGroup(otherGroup);
729
730      // The unrelated explicit move above must not have disturbed the regex-governed group.
731      assertTrue(ADMIN.getRSGroup(groupName).getServers().contains(addr));
732    } finally {
733      removeRegexGroup(groupName);
734    }
735  }
736
737  @Test
738  public void testRefreshRemovesRegexMatchedServerFromStoredAdminGroup() throws Exception {
739    // A server stored in an admin-managed group before a regex claimed it must not end up in two
740    // groups after a manager restart. refresh cannot reject (it would wedge startup), so the regex
741    // wins in memory and a WARN is logged.
742    String adminGroup = getGroupName("refreshadmin");
743    String regexGroup = getGroupName("refreshregex");
744    ADMIN.addRSGroup(adminGroup);
745    ADMIN.addRSGroup(regexGroup);
746    LogCapturer capturer = captureRSGroupInfoManagerLog();
747    try {
748      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
749      Address addr = addressOf(rst);
750      TEST_UTIL.waitFor(WAIT_TIMEOUT,
751        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
752      ADMIN.moveServersToRSGroup(Sets.newHashSet(addr), adminGroup);
753
754      // The regex arrives while the server is already stored in the admin group. A new master
755      // copies the minicluster configuration, so set it there too before the failover.
756      setRegex(regexGroup, "127\\.0\\.0\\..*");
757      TEST_UTIL.getMiniHBaseCluster().getConfiguration().set(REGEX_PREFIX + regexGroup,
758        "127\\.0\\.0\\..*");
759      restartMaster();
760      TEST_UTIL.waitFor(WAIT_TIMEOUT,
761        () -> ADMIN.getRSGroup(regexGroup).getServers().contains(addr));
762      assertFalse(ADMIN.getRSGroup(adminGroup).getServers().contains(addr));
763      assertFalse(ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
764      assertTrue(capturer.getOutput().contains("are stored in RSGroup '" + adminGroup + "'"));
765
766      // The startup flush clears the stale stored copy: the admin group no longer holds the server
767      // and the regex-governed group stores no servers at all.
768      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> !readStoredServers(adminGroup).contains(addr));
769      assertTrue(readStoredServers(regexGroup).isEmpty());
770    } finally {
771      capturer.stopCapturing();
772      TEST_UTIL.getMiniHBaseCluster().getConfiguration().unset(REGEX_PREFIX + regexGroup);
773      removeRegexGroup(regexGroup);
774      removeGroup(adminGroup);
775    }
776  }
777
778  @Test
779  public void testRegexConfigDriftDoesNotBlockUnrelatedAdminOps() throws Exception {
780    // A regex that contradicts where a server was explicitly placed must not block any unrelated
781    // admin operation.
782    String groupName = getGroupName("drift");
783    String bystanderGroup = getGroupName("bystander");
784    ADMIN.addRSGroup(groupName);
785    ADMIN.addRSGroup(bystanderGroup);
786    String renamedBystander = bystanderGroup + "_renamed";
787    try {
788      JVMClusterUtil.RegionServerThread rst = startFakeHostnameRS("127.0.0.1");
789      Address addr = addressOf(rst);
790      TEST_UTIL.waitFor(WAIT_TIMEOUT,
791        () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr));
792
793      // Legal at the time: no regex governs groupName yet.
794      ADMIN.moveServersToRSGroup(Sets.newHashSet(addr), groupName);
795
796      // Now introduce a regex for groupName that does NOT match the server already placed there.
797      // No listener cycle runs, so the contradiction stays un-reconciled.
798      setRegex(groupName, "10\\.0\\.0\\..*");
799
800      ADMIN.renameRSGroup(bystanderGroup, renamedBystander);
801      assertTrue(ADMIN.listRSGroups().stream().anyMatch(g -> g.getName().equals(renamedBystander)));
802    } finally {
803      removeRegexGroup(groupName);
804      removeGroup(ADMIN.getRSGroup(bystanderGroup) != null ? bystanderGroup : renamedBystander);
805    }
806  }
807
808  @Test
809  public void testNewlyJoinedRegexMemberReceivesRegionsOnBalance() throws Exception {
810    // A new RS auto-joins an already-regex-governed group while the group's sole existing
811    // member holds every region of a table bound to that group; running the balancer must then
812    // hand some of those regions to the newly-joined RS too. The first-member baseline is
813    // established by setRSGroup itself: RSGroupInfoManagerImpl#setRSGroup synchronously relocates
814    // every region of a table with no RSGroup-scoped descriptor yet onto the target group's
815    // servers (see moveTablesAndWait, which blocks on each ModifyTableProcedure before returning),
816    // so with sn1 as the sole group member at that point all regions are already there once
817    // setRSGroup returns -- no separate balance step is needed for that part. Only the
818    // second-member handoff genuinely needs an explicit balance: adding a group member does not
819    // by itself retrigger any region movement.
820    String groupName = getGroupName("balancenewmember");
821    ADMIN.addRSGroup(groupName);
822    // "127.0.0.1" and "localhost" are the only two distinct, real, independently-reachable
823    // identities startFakeHostnameRS supports (its RPC bind address is always "localhost"
824    // regardless of advertised hostname).
825    setRegex(groupName, "127\\.0\\.0\\.1|localhost");
826    try {
827      JVMClusterUtil.RegionServerThread rst1 = startFakeHostnameRS("127.0.0.1");
828      Address addr1 = addressOf(rst1);
829      TEST_UTIL.waitFor(WAIT_TIMEOUT,
830        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr1));
831      ServerName sn1 = getServerName(addr1);
832
833      TEST_UTIL.createMultiRegionTable(tableName, Bytes.toBytes("f"), 10);
834      TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
835      ADMIN.setRSGroup(Sets.newHashSet(tableName), groupName);
836      Map<ServerName, List<String>> perServerAfterMove = getTableServerRegionMap().get(tableName);
837      assertEquals(10, perServerAfterMove.get(sn1).size());
838
839      ADMIN.balancerSwitch(true, true);
840      try {
841        // A second RS matching the same regex joins while the group is already active; it must
842        // auto-join with no explicit RPC.
843        JVMClusterUtil.RegionServerThread rst2 = startFakeHostnameRS("localhost");
844        Address addr2 = addressOf(rst2);
845        TEST_UTIL.waitFor(WAIT_TIMEOUT,
846          () -> ADMIN.getRSGroup(groupName).getServers().contains(addr2));
847        ServerName sn2 = getServerName(addr2);
848
849        ADMIN.balanceRSGroup(groupName);
850        TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> {
851          Map<ServerName, List<String>> perServer = getTableServerRegionMap().get(tableName);
852          if (perServer == null) {
853            return false;
854          }
855          List<String> onNewMember = perServer.get(sn2);
856          if (onNewMember == null || onNewMember.isEmpty()) {
857            return false;
858          }
859          int total = perServer.values().stream().mapToInt(List::size).sum();
860          return total == 10;
861        });
862      } finally {
863        ADMIN.balancerSwitch(false, true);
864      }
865    } finally {
866      removeRegexGroup(groupName);
867    }
868  }
869
870  @Test
871  public void testRestartTriggeredMembershipConfinesTableToGroupServers() throws Exception {
872    // RS join normally (no regex active yet), landing in default like any other RS. Only
873    // afterward is a regex configured targeting their hostnames; a bare config change does not
874    // reconcile membership by itself, so an actual lifecycle event -- restarting one of the two
875    // RS -- is used to force the recompute to notice and migrate both into the new group
876    // (the recompute triggered by one server's add/remove event considers every online server,
877    // not just the one that triggered it). A table is then bound to that group, and its regions
878    // must land only on the group's members, with those members hosting only that table's
879    // regions -- a bidirectional isolation check.
880    String groupName = getGroupName("restartisolation");
881    // "127.0.0.1" and "localhost" are the only two distinct, real, independently-reachable
882    // identities startFakeHostnameRS supports (its RPC bind address is always "localhost"
883    // regardless of advertised hostname -- an arbitrary loopback alias like "127.0.0.2" would
884    // advertise an address nothing is actually listening on, stalling any real region-open RPC).
885    JVMClusterUtil.RegionServerThread rst1 = startFakeHostnameRS("127.0.0.1");
886    JVMClusterUtil.RegionServerThread rst2 = startFakeHostnameRS("localhost");
887    Address addr2 = addressOf(rst2);
888    TEST_UTIL.waitFor(WAIT_TIMEOUT,
889      () -> ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers().contains(addr2));
890
891    ADMIN.addRSGroup(groupName);
892    setRegex(groupName, "127\\.0\\.0\\.1|localhost");
893    try {
894      stopFakeRegionServer(rst1);
895      JVMClusterUtil.RegionServerThread rst1Restarted = startFakeHostnameRS("127.0.0.1");
896      Address addr1Restarted = addressOf(rst1Restarted);
897
898      TEST_UTIL.waitFor(WAIT_TIMEOUT,
899        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr1Restarted));
900      TEST_UTIL.waitFor(WAIT_TIMEOUT,
901        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr2));
902
903      ServerName sn1 = getServerName(addr1Restarted);
904      ServerName sn2 = getServerName(addr2);
905
906      TEST_UTIL.createMultiRegionTable(tableName, Bytes.toBytes("f"), 6);
907      TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
908      // setRSGroup synchronously relocates every region of a table with no RSGroup-scoped
909      // descriptor yet onto one of the target group's servers (moveTablesAndWait blocks on each
910      // ModifyTableProcedure before returning), so confinement to {sn1, sn2} is already guaranteed
911      // once this call returns -- no separate balance step is needed.
912      ADMIN.setRSGroup(Sets.newHashSet(tableName), groupName);
913      Map<ServerName, List<String>> perServerAfterMove = getTableServerRegionMap().get(tableName);
914      int totalAfterMove = 0;
915      for (Map.Entry<ServerName, List<String>> entry : perServerAfterMove.entrySet()) {
916        assertTrue(entry.getKey().equals(sn1) || entry.getKey().equals(sn2));
917        totalAfterMove += entry.getValue().size();
918      }
919      assertEquals(6, totalAfterMove);
920
921      // Bidirectional isolation: the group's own members must host nothing but this table.
922      for (RegionInfo region : ADMIN.getRegions(sn1)) {
923        assertEquals(tableName, region.getTable());
924      }
925      for (RegionInfo region : ADMIN.getRegions(sn2)) {
926        assertEquals(tableName, region.getTable());
927      }
928    } finally {
929      removeRegexGroup(groupName);
930    }
931  }
932
933  @Test
934  public void testUngracefulCrashOfRegexMemberReassignsRegionsOnlyToOtherGroupMembers()
935    throws Exception {
936    // ServerCrashProcedure recovers a dead server's regions by default with
937    // hbase.master.scp.retain.assignment=false, i.e. forceNewPlan=true for every ASSIGN TRSP it
938    // creates -- which routes through roundRobinAssignment rather than retainAssignment (see
939    // ServerCrashProcedure#assignRegions). This test's job is to confirm
940    // RSGroupBasedLoadBalancer#roundRobinAssignment still confines the
941    // crashed regex member's regions to the *other* online members of its own group -- never to
942    // default or an unrelated group -- rather than trusting the RSGroup-partitioning logic to
943    // "just work". It also asserts, via the test-only call-tracking flags, exactly which
944    // assignment methods actually ran for each step: retainAssignment during
945    // setRSGroup, and roundRobinAssignment (not retainAssignment) during crash recovery.
946    String groupName = getGroupName("crashroundrobin");
947    RSGroupBasedLoadBalancer balancer = (RSGroupBasedLoadBalancer) MASTER.getLoadBalancer();
948    ADMIN.addRSGroup(groupName);
949    // "127.0.0.1" and "localhost" are the only two distinct, real, independently-reachable
950    // identities startFakeHostnameRS supports (its RPC bind address is always "localhost"
951    // regardless of advertised hostname).
952    setRegex(groupName, "127\\.0\\.0\\.1|localhost");
953    // A second, unrelated RSGroup holding one server borrowed from default -- proves crash
954    // recovery's per-group candidate filtering excludes every other group, not just default.
955    String otherGroupName = getGroupName("crashroundrobinother");
956    RSGroupInfo otherGroupInfo = addGroup(otherGroupName, 1);
957    ServerName otherGroupServer = getServerName(otherGroupInfo.getServers().iterator().next());
958    try {
959      JVMClusterUtil.RegionServerThread rst1 = startFakeHostnameRS("127.0.0.1");
960      Address addr1 = addressOf(rst1);
961      TEST_UTIL.waitFor(WAIT_TIMEOUT,
962        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr1));
963      ServerName sn1 = getServerName(addr1);
964
965      JVMClusterUtil.RegionServerThread rst2 = startFakeHostnameRS("localhost");
966      Address addr2 = addressOf(rst2);
967      TEST_UTIL.waitFor(WAIT_TIMEOUT,
968        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr2));
969      ServerName sn2 = getServerName(addr2);
970
971      TEST_UTIL.createMultiRegionTable(tableName, Bytes.toBytes("f"), 6);
972      TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
973      // setRSGroup synchronously relocates every region of a table with no RSGroup-scoped
974      // descriptor yet onto the target group's servers (moveTablesAndWait blocks on the
975      // ModifyTableProcedure, whose child ReopenTableRegionsProcedure blocks on the reopened
976      // regions), so confinement to {sn1, sn2} is already guaranteed once this call returns.
977      balancer.resetAssignmentCallFlagsForTest();
978      ADMIN.setRSGroup(Sets.newHashSet(tableName), groupName);
979      // ReopenTableRegionsProcedure closes and reassigns every region via a single batched
980      // retainAssignment call; since none of these regions' previous hosts are group members,
981      // the wrapped internal balancer's own retain logic (not this class's randomAssignment)
982      // places them on random candidate hosts within the group -- see BaseLoadBalancer's
983      // "assigned to random hosts" log path invoked from within retainAssignment.
984      assertFalse(balancer.isRandomAssignmentInvoked());
985      assertTrue(balancer.isRetainAssignmentInvoked());
986      assertFalse(balancer.isRoundRobinAssignmentInvoked());
987      Map<ServerName, List<String>> perServerBeforeCrash = getTableServerRegionMap().get(tableName);
988      int totalBeforeCrash = 0;
989      for (Map.Entry<ServerName, List<String>> entry : perServerBeforeCrash.entrySet()) {
990        assertTrue(entry.getKey().equals(sn1) || entry.getKey().equals(sn2));
991        totalBeforeCrash += entry.getValue().size();
992      }
993      assertEquals(6, totalBeforeCrash);
994
995      // retainAssignment's random fallback (BaseLoadBalancer#randomAssignment) places each of
996      // these regions independently, so it is possible -- roughly 3% of runs -- for all 6 to land
997      // on one of the two servers alone. Crash whichever server actually has regions, so
998      // ServerCrashProcedure always has at least one region to reassign and roundRobinAssignment
999      // is guaranteed to fire; default to the usual sn1-crashes/sn2-survives case otherwise.
1000      boolean sn1HasNoRegions =
1001        !perServerBeforeCrash.containsKey(sn1) || perServerBeforeCrash.get(sn1).isEmpty();
1002      JVMClusterUtil.RegionServerThread rstToCrash = sn1HasNoRegions ? rst2 : rst1;
1003      ServerName survivor = sn1HasNoRegions ? sn1 : sn2;
1004
1005      // Simulate an ungraceful crash
1006      balancer.resetAssignmentCallFlagsForTest();
1007      killFakeRegionServer(rstToCrash);
1008
1009      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> {
1010        Map<ServerName, List<String>> perServer = getTableServerRegionMap().get(tableName);
1011        if (perServer == null) {
1012          return false;
1013        }
1014        int total = 0;
1015        for (Map.Entry<ServerName, List<String>> entry : perServer.entrySet()) {
1016          // Every region must have landed on the group's sole remaining member -- never on
1017          // default or any other group, and never back on the now-dead server.
1018          if (!entry.getKey().equals(survivor)) {
1019            return false;
1020          }
1021          total += entry.getValue().size();
1022        }
1023        return total == 6;
1024      });
1025      // Confirms the crash-recovery ASSIGN TRSPs went through roundRobinAssignment, not
1026      // retainAssignment -- expected since hbase.master.scp.retain.assignment defaults to false.
1027      assertTrue(balancer.isRoundRobinAssignmentInvoked());
1028      assertFalse(balancer.isRetainAssignmentInvoked());
1029
1030      // The surviving group member must host nothing but this table's regions.
1031      for (RegionInfo region : ADMIN.getRegions(survivor)) {
1032        assertEquals(tableName, region.getTable());
1033      }
1034      // The unrelated group's own member must not have received any of the crashed member's
1035      // regions either -- recovery must stay confined to the crashed server's own group.
1036      for (RegionInfo region : ADMIN.getRegions(otherGroupServer)) {
1037        assertFalse(region.getTable().equals(tableName));
1038      }
1039    } finally {
1040      removeRegexGroup(groupName);
1041      removeGroup(otherGroupName);
1042    }
1043  }
1044
1045  @Test
1046  public void testFallbackWhenAllRegexMembersCrash() throws Exception {
1047    // RSGroupBasedLoadBalancer#generateGroupAssignments only ever looks at a group's currently
1048    // online configured servers, regardless of how they became members -- regex-driven or
1049    // admin-driven membership is indistinguishable to it. This confirms the
1050    // hbase.rsgroup.fallback.enable=true fallback-to-default path works the same way when the
1051    // group that loses every one of its servers is regex-governed.
1052    String groupName = getGroupName("fallbackregex");
1053    ADMIN.addRSGroup(groupName);
1054    // "127.0.0.1" and "localhost" are the only two distinct, real, independently-reachable
1055    // identities startFakeHostnameRS supports (its RPC bind address is always "localhost"
1056    // regardless of advertised hostname).
1057    setRegex(groupName, "127\\.0\\.0\\.1|localhost");
1058    RSGroupBasedLoadBalancer balancer = (RSGroupBasedLoadBalancer) MASTER.getLoadBalancer();
1059    balancer.setFallbackEnabledForTest(true);
1060    try {
1061      JVMClusterUtil.RegionServerThread rst1 = startFakeHostnameRS("127.0.0.1");
1062      Address addr1 = addressOf(rst1);
1063      TEST_UTIL.waitFor(WAIT_TIMEOUT,
1064        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr1));
1065      ServerName sn1 = getServerName(addr1);
1066
1067      JVMClusterUtil.RegionServerThread rst2 = startFakeHostnameRS("localhost");
1068      Address addr2 = addressOf(rst2);
1069      TEST_UTIL.waitFor(WAIT_TIMEOUT,
1070        () -> ADMIN.getRSGroup(groupName).getServers().contains(addr2));
1071      ServerName sn2 = getServerName(addr2);
1072
1073      TEST_UTIL.createMultiRegionTable(tableName, Bytes.toBytes("f"), 6);
1074      TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
1075      // setRSGroup synchronously relocates every region of a table with no RSGroup-scoped
1076      // descriptor yet onto the target group's servers (moveTablesAndWait blocks on each
1077      // ModifyTableProcedure before returning), so confinement to {sn1, sn2} is already guaranteed
1078      // once this call returns.
1079      ADMIN.setRSGroup(Sets.newHashSet(tableName), groupName);
1080
1081      // Both regex members must be hosting regions of the table before either is killed.
1082      Map<ServerName, List<String>> perServerBeforeCrash = getTableServerRegionMap().get(tableName);
1083      int totalBeforeCrash = 0;
1084      for (ServerName member : new ServerName[] { sn1, sn2 }) {
1085        List<String> regionsOnMember = perServerBeforeCrash.get(member);
1086        assertFalse(regionsOnMember == null || regionsOnMember.isEmpty());
1087        totalBeforeCrash += regionsOnMember.size();
1088      }
1089      assertEquals(6, totalBeforeCrash);
1090
1091      // Kill both regex members -- the group is left with zero online servers. Unlike the base
1092      // cluster's servers (which remain in default throughout this test), these two fake RS were
1093      // never part of default, so default keeps its original online servers and the fallback
1094      // path lands there directly, with no need for a second-level fallback.
1095      killFakeRegionServer(rst1);
1096      killFakeRegionServer(rst2);
1097
1098      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> {
1099        Map<ServerName, List<String>> perServer = getTableServerRegionMap().get(tableName);
1100        if (perServer == null) {
1101          return false;
1102        }
1103        RSGroupInfo defaultGroup;
1104        try {
1105          defaultGroup = ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP);
1106        } catch (IOException e) {
1107          return false;
1108        }
1109        int total = 0;
1110        for (Map.Entry<ServerName, List<String>> entry : perServer.entrySet()) {
1111          // Every region must have landed on a server that is currently in default -- never left
1112          // stranded on the now-dead sn1/sn2, and never on any unrelated group.
1113          if (!defaultGroup.getServers().contains(entry.getKey().getAddress())) {
1114            return false;
1115          }
1116          total += entry.getValue().size();
1117        }
1118        return total == 6;
1119      });
1120    } finally {
1121      balancer.setFallbackEnabledForTest(false);
1122      removeRegexGroup(groupName);
1123    }
1124  }
1125
1126  @Test
1127  public void testExplicitMoveFromDefaultToAdminGroupTriggersRandomAssignmentBackToDefault()
1128    throws Exception {
1129    // Three concurrently active RSGroups -- default, a regex-governed group, and an
1130    // admin-managed group -- each with at least one member. Explicitly moving a server out of
1131    // default into the admin-managed group (RSGroupInfoManagerImpl#moveServers) leaves that
1132    // server still hosting regions of a table that belongs to default; moveServerRegionsFromGroup
1133    // detects the mismatch and relocates those orphaned regions back onto default's *remaining*
1134    // members via LoadBalancer#randomAssignment, then actually executes each relocation through
1135    // AssignmentManager#moveAsync -- which, because the destination is already decided, creates a
1136    // MOVE-type TransitRegionStateProcedure whose queueAssign sets a non-null region location, so
1137    // AssignmentManager#processAssignQueue routes it into retainMap and retainAssignment also
1138    // fires. roundRobinAssignment never fires, since no region in this flow is ever left with a
1139    // null location. This test confirms that exact combination of calls, plus the final,
1140    // fully-partitioned region placement across all three groups.
1141    RSGroupBasedLoadBalancer balancer = (RSGroupBasedLoadBalancer) MASTER.getLoadBalancer();
1142    String regexGroupName = getGroupName("regex3way");
1143    String adminGroupName = getGroupName("admin3way");
1144    ADMIN.addRSGroup(regexGroupName);
1145    // "127.0.0.1" is the only fake identity needed here; it is never a member of default, so it
1146    // cannot interfere with the base cluster's servers used below.
1147    setRegex(regexGroupName, "127\\.0\\.0\\.1");
1148    try {
1149      JVMClusterUtil.RegionServerThread regexRst = startFakeHostnameRS("127.0.0.1");
1150      Address regexAddr = addressOf(regexRst);
1151      TEST_UTIL.waitFor(WAIT_TIMEOUT,
1152        () -> ADMIN.getRSGroup(regexGroupName).getServers().contains(regexAddr));
1153
1154      // Admin-managed group starts with one server borrowed from default -- NUM_SLAVES_BASE(4)
1155      // - 1 = 3 remain in default, enough that moving one more away below still satisfies
1156      // moveServers' KEEP_ONE_SERVER_IN_DEFAULT_ERROR_MESSAGE constraint (default must always
1157      // retain more servers than are being moved out of it).
1158      RSGroupInfo adminGroupInfo = addGroup(adminGroupName, 1);
1159      Address adminServerInitialAddr = adminGroupInfo.getServers().iterator().next();
1160
1161      // A table with no explicit RSGroup is implicitly confined to default's current members --
1162      // here, the 3 servers remaining in default after the admin group's initial carve-out.
1163      TEST_UTIL.createMultiRegionTable(tableName, Bytes.toBytes("f"), 9);
1164      TEST_UTIL.waitUntilAllRegionsAssigned(tableName);
1165
1166      // Pick a default-group server that is actually hosting some of this table's regions -- the
1167      // server we are about to move, so the orphaned-region relocation has something to do.
1168      RSGroupInfo defaultGroupBeforeMove = ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP);
1169      Map<ServerName, List<String>> perServerBeforeMove = getTableServerRegionMap().get(tableName);
1170      int totalRegionsBeforeMove = 0;
1171      ServerName movingServer = null;
1172      for (Map.Entry<ServerName, List<String>> entry : perServerBeforeMove.entrySet()) {
1173        assertTrue(defaultGroupBeforeMove.getServers().contains(entry.getKey().getAddress()));
1174        totalRegionsBeforeMove += entry.getValue().size();
1175        if (movingServer == null && !entry.getValue().isEmpty()) {
1176          movingServer = entry.getKey();
1177        }
1178      }
1179      assertEquals(9, totalRegionsBeforeMove);
1180      Address movingAddr = movingServer.getAddress();
1181      Set<Address> defaultServersBeforeMove = new HashSet<>(defaultGroupBeforeMove.getServers());
1182
1183      // The action under test: explicitly move that one server out of default into the
1184      // admin-managed group.
1185      balancer.resetAssignmentCallFlagsForTest();
1186      ADMIN.moveServersToRSGroup(Sets.newHashSet(movingAddr), adminGroupName);
1187
1188      // See the method comment above for why both randomAssignment (picking the relocation plan)
1189      // and retainAssignment (executing it via moveAsync) fire, while roundRobinAssignment never
1190      // does.
1191      assertTrue(balancer.isRandomAssignmentInvoked());
1192      assertTrue(balancer.isRetainAssignmentInvoked());
1193      assertFalse(balancer.isRoundRobinAssignmentInvoked());
1194
1195      // Membership: the moved server now belongs to the admin-managed group, not default; the
1196      // regex group and the rest of default are untouched by this move.
1197      Set<Address> expectedDefaultServersAfterMove = new HashSet<>(defaultServersBeforeMove);
1198      expectedDefaultServersAfterMove.remove(movingAddr);
1199      assertEquals(expectedDefaultServersAfterMove,
1200        ADMIN.getRSGroup(RSGroupInfo.DEFAULT_GROUP).getServers());
1201      assertEquals(Sets.newHashSet(adminServerInitialAddr, movingAddr),
1202        ADMIN.getRSGroup(adminGroupName).getServers());
1203      assertEquals(Sets.newHashSet(regexAddr), ADMIN.getRSGroup(regexGroupName).getServers());
1204
1205      // Regions: every one of the table's 9 regions must have moved off the relocated server,
1206      // landing only on default's remaining members -- never on the admin-managed or
1207      // regex-governed group's servers. moveServers blocks until this relocation completes, but
1208      // poll defensively rather than asserting immediately.
1209      TEST_UTIL.waitFor(WAIT_TIMEOUT, () -> {
1210        Map<ServerName, List<String>> perServer = getTableServerRegionMap().get(tableName);
1211        if (perServer == null) {
1212          return false;
1213        }
1214        int total = 0;
1215        for (Map.Entry<ServerName, List<String>> entry : perServer.entrySet()) {
1216          if (!expectedDefaultServersAfterMove.contains(entry.getKey().getAddress())) {
1217            return false;
1218          }
1219          total += entry.getValue().size();
1220        }
1221        return total == 9;
1222      });
1223
1224      // Bidirectional isolation, now that the move has settled: neither the admin-managed group's
1225      // servers (the original member and the one just moved in) nor the regex group's server host
1226      // any region of this default-group table.
1227      for (Address addr : ADMIN.getRSGroup(adminGroupName).getServers()) {
1228        for (RegionInfo region : ADMIN.getRegions(getServerName(addr))) {
1229          assertFalse(region.getTable().equals(tableName));
1230        }
1231      }
1232      for (RegionInfo region : ADMIN.getRegions(getServerName(regexAddr))) {
1233        assertFalse(region.getTable().equals(tableName));
1234      }
1235    } finally {
1236      removeRegexGroup(regexGroupName);
1237      removeGroup(adminGroupName);
1238    }
1239  }
1240}