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}