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.master.procedure; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertFalse; 022import static org.junit.Assert.assertTrue; 023 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.List; 027import java.util.Objects; 028import java.util.stream.Collectors; 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.hbase.HBaseClassTestRule; 031import org.apache.hadoop.hbase.HBaseTestingUtil; 032import org.apache.hadoop.hbase.SingleProcessHBaseCluster; 033import org.apache.hadoop.hbase.TableName; 034import org.apache.hadoop.hbase.UnknownRegionException; 035import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 036import org.apache.hadoop.hbase.client.RegionInfo; 037import org.apache.hadoop.hbase.client.Table; 038import org.apache.hadoop.hbase.client.TableDescriptor; 039import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 040import org.apache.hadoop.hbase.procedure2.Procedure; 041import org.apache.hadoop.hbase.procedure2.ProcedureExecutor; 042import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility; 043import org.apache.hadoop.hbase.testclassification.MasterTests; 044import org.apache.hadoop.hbase.testclassification.MediumTests; 045import org.apache.hadoop.hbase.util.Bytes; 046import org.junit.AfterClass; 047import org.junit.BeforeClass; 048import org.junit.ClassRule; 049import org.junit.Test; 050import org.junit.experimental.categories.Category; 051 052@Category({ MasterTests.class, MediumTests.class }) 053public class TestReopenTableRegionsProcedureSpecificRegions { 054 055 @ClassRule 056 public static final HBaseClassTestRule CLASS_RULE = 057 HBaseClassTestRule.forClass(TestReopenTableRegionsProcedureSpecificRegions.class); 058 059 private static final HBaseTestingUtil UTIL = new HBaseTestingUtil(); 060 private static final byte[] CF = Bytes.toBytes("cf"); 061 062 private static SingleProcessHBaseCluster singleProcessHBaseCluster; 063 064 @BeforeClass 065 public static void setupCluster() throws Exception { 066 Configuration conf = UTIL.getConfiguration(); 067 conf.setInt(MasterProcedureConstants.MASTER_PROCEDURE_THREADS, 1); 068 singleProcessHBaseCluster = UTIL.startMiniCluster(1); 069 } 070 071 @AfterClass 072 public static void tearDown() throws Exception { 073 UTIL.shutdownMiniCluster(); 074 if (Objects.nonNull(singleProcessHBaseCluster)) { 075 singleProcessHBaseCluster.close(); 076 } 077 } 078 079 private ProcedureExecutor<MasterProcedureEnv> getProcExec() { 080 return UTIL.getMiniHBaseCluster().getMaster().getMasterProcedureExecutor(); 081 } 082 083 @Test 084 public void testInvalidRegionNamesThrowsException() throws Exception { 085 TableName tableName = TableName.valueOf("TestInvalidRegions"); 086 try (Table ignored = UTIL.createTable(tableName, CF)) { 087 088 List<RegionInfo> regions = UTIL.getAdmin().getRegions(tableName); 089 assertFalse("Table should have at least one region", regions.isEmpty()); 090 091 List<byte[]> invalidRegionNames = 092 Collections.singletonList(Bytes.toBytes("non-existent-region-name")); 093 094 ReopenTableRegionsProcedure proc = 095 new ReopenTableRegionsProcedure(tableName, invalidRegionNames, 0L, Integer.MAX_VALUE); 096 097 long procId = getProcExec().submitProcedure(proc); 098 UTIL.waitFor(60000, proc::isFailed); 099 100 Throwable cause = ProcedureTestingUtility.getExceptionCause(proc); 101 assertTrue("Expected UnknownRegionException, got: " + cause.getClass().getName(), 102 cause instanceof UnknownRegionException); 103 assertTrue("Error message should contain region name", 104 cause.getMessage().contains("non-existent-region-name")); 105 assertTrue("Error message should contain table name", 106 cause.getMessage().contains(tableName.getNameAsString())); 107 } 108 } 109 110 @Test 111 public void testMixedValidInvalidRegions() throws Exception { 112 TableName tableName = TableName.valueOf("TestMixedRegions"); 113 try (Table ignored = UTIL.createTable(tableName, CF)) { 114 115 List<RegionInfo> actualRegions = UTIL.getAdmin().getRegions(tableName); 116 assertFalse("Table should have at least one region", actualRegions.isEmpty()); 117 118 List<byte[]> mixedRegionNames = new ArrayList<>(); 119 mixedRegionNames.add(actualRegions.get(0).getRegionName()); 120 mixedRegionNames.add(Bytes.toBytes("invalid-region-1")); 121 mixedRegionNames.add(Bytes.toBytes("invalid-region-2")); 122 123 ReopenTableRegionsProcedure proc = 124 new ReopenTableRegionsProcedure(tableName, mixedRegionNames, 0L, Integer.MAX_VALUE); 125 126 long procId = getProcExec().submitProcedure(proc); 127 UTIL.waitFor(60000, proc::isFailed); 128 129 Throwable cause = ProcedureTestingUtility.getExceptionCause(proc); 130 assertTrue("Expected UnknownRegionException", cause instanceof UnknownRegionException); 131 assertTrue("Error message should contain first invalid region", 132 cause.getMessage().contains("invalid-region-1")); 133 assertTrue("Error message should contain second invalid region", 134 cause.getMessage().contains("invalid-region-2")); 135 } 136 } 137 138 @Test 139 public void testSpecificRegionsReopenWithThrottling() throws Exception { 140 TableName tableName = TableName.valueOf("TestSpecificThrottled"); 141 142 TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName) 143 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(CF)) 144 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_BACKOFF_MILLIS_KEY, "100") 145 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_SIZE_MAX_KEY, "2").build(); 146 147 UTIL.getAdmin().createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 5); 148 149 List<RegionInfo> allRegions = UTIL.getAdmin().getRegions(tableName); 150 assertEquals(5, allRegions.size()); 151 152 List<byte[]> specificRegionNames = 153 allRegions.subList(0, 3).stream().map(RegionInfo::getRegionName).collect(Collectors.toList()); 154 155 ReopenTableRegionsProcedure proc = ReopenTableRegionsProcedure.throttled( 156 UTIL.getConfiguration(), UTIL.getAdmin().getDescriptor(tableName), specificRegionNames); 157 158 long procId = getProcExec().submitProcedure(proc); 159 ProcedureTestingUtility.waitProcedure(getProcExec(), procId); 160 161 assertFalse("Procedure should succeed", proc.isFailed()); 162 assertEquals("Should reopen exactly 3 regions", 3, proc.getRegionsReopened()); 163 assertTrue("Should process multiple batches with batch size 2", 164 proc.getBatchesProcessed() >= 2); 165 } 166 167 @Test 168 public void testEmptyRegionListReopensAll() throws Exception { 169 TableName tableName = TableName.valueOf("TestEmptyList"); 170 171 TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName) 172 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(CF)).build(); 173 174 UTIL.getAdmin().createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 5); 175 176 List<RegionInfo> allRegions = UTIL.getAdmin().getRegions(tableName); 177 assertEquals(5, allRegions.size()); 178 179 ReopenTableRegionsProcedure proc = ReopenTableRegionsProcedure 180 .throttled(UTIL.getConfiguration(), UTIL.getAdmin().getDescriptor(tableName)); 181 182 long procId = getProcExec().submitProcedure(proc); 183 ProcedureTestingUtility.waitProcedure(getProcExec(), procId); 184 185 assertFalse("Procedure should succeed", proc.isFailed()); 186 assertEquals("Should reopen all 5 regions", 5, proc.getRegionsReopened()); 187 } 188 189 @Test 190 public void testDisabledTableSkipsReopen() throws Exception { 191 TableName tableName = TableName.valueOf("TestDisabledTable"); 192 try (Table ignored = UTIL.createTable(tableName, CF)) { 193 UTIL.getAdmin().disableTable(tableName); 194 195 ReopenTableRegionsProcedure proc = ReopenTableRegionsProcedure 196 .throttled(UTIL.getConfiguration(), UTIL.getAdmin().getDescriptor(tableName)); 197 198 long procId = getProcExec().submitProcedure(proc); 199 ProcedureTestingUtility.waitProcedure(getProcExec(), procId); 200 201 assertFalse("Procedure should succeed", proc.isFailed()); 202 assertEquals("Should not reopen any regions for disabled table", 0, 203 proc.getRegionsReopened()); 204 } 205 } 206 207 @Test 208 public void testReopenRegionsThrottledWithLargeTable() throws Exception { 209 TableName tableName = TableName.valueOf("TestLargeTable"); 210 211 TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName) 212 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(CF)) 213 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_BACKOFF_MILLIS_KEY, "50") 214 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_SIZE_MAX_KEY, "3").build(); 215 216 UTIL.getAdmin().createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 10); 217 218 List<RegionInfo> regions = UTIL.getAdmin().getRegions(tableName); 219 assertEquals(10, regions.size()); 220 221 ReopenTableRegionsProcedure proc = ReopenTableRegionsProcedure 222 .throttled(UTIL.getConfiguration(), UTIL.getAdmin().getDescriptor(tableName)); 223 224 long procId = getProcExec().submitProcedure(proc); 225 ProcedureTestingUtility.waitProcedure(getProcExec(), procId); 226 227 assertFalse("Procedure should succeed", proc.isFailed()); 228 assertEquals("Should reopen all 10 regions", 10, proc.getRegionsReopened()); 229 assertTrue("Should process multiple batches", proc.getBatchesProcessed() >= 4); 230 } 231 232 @Test 233 public void testConfigurationPrecedence() throws Exception { 234 TableName tableName = TableName.valueOf("TestConfigPrecedence"); 235 236 Configuration conf = UTIL.getConfiguration(); 237 conf.setLong(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_BACKOFF_MILLIS_KEY, 1000); 238 conf.setInt(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_SIZE_MAX_KEY, 5); 239 240 TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName) 241 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(CF)) 242 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_BACKOFF_MILLIS_KEY, "2000") 243 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_SIZE_MAX_KEY, "2").build(); 244 245 UTIL.getAdmin().createTable(td); 246 247 ReopenTableRegionsProcedure proc = 248 ReopenTableRegionsProcedure.throttled(conf, UTIL.getAdmin().getDescriptor(tableName)); 249 250 assertEquals("Table descriptor config should override global config", 2000, 251 proc.getReopenBatchBackoffMillis()); 252 } 253 254 @Test 255 public void testThrottledVsUnthrottled() throws Exception { 256 TableName tableName = TableName.valueOf("TestThrottledVsUnthrottled"); 257 258 TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName) 259 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(CF)) 260 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_BACKOFF_MILLIS_KEY, "1000") 261 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_SIZE_MAX_KEY, "2").build(); 262 263 UTIL.getAdmin().createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 5); 264 265 List<RegionInfo> regions = UTIL.getAdmin().getRegions(tableName); 266 List<byte[]> regionNames = 267 regions.stream().map(RegionInfo::getRegionName).collect(Collectors.toList()); 268 269 ReopenTableRegionsProcedure unthrottledProc = 270 new ReopenTableRegionsProcedure(tableName, regionNames); 271 assertEquals("Unthrottled should use default (0ms)", 0, 272 unthrottledProc.getReopenBatchBackoffMillis()); 273 274 ReopenTableRegionsProcedure throttledProc = ReopenTableRegionsProcedure 275 .throttled(UTIL.getConfiguration(), UTIL.getAdmin().getDescriptor(tableName), regionNames); 276 assertEquals("Throttled should use table config (1000ms)", 1000, 277 throttledProc.getReopenBatchBackoffMillis()); 278 } 279 280 @Test 281 public void testExceptionInProcedureExecution() throws Exception { 282 TableName tableName = TableName.valueOf("TestExceptionInExecution"); 283 try (Table ignored = UTIL.createTable(tableName, CF)) { 284 285 List<byte[]> invalidRegionNames = 286 Collections.singletonList(Bytes.toBytes("nonexistent-region")); 287 288 ReopenTableRegionsProcedure proc = 289 new ReopenTableRegionsProcedure(tableName, invalidRegionNames, 0L, Integer.MAX_VALUE); 290 291 long procId = getProcExec().submitProcedure(proc); 292 UTIL.waitFor(60000, () -> getProcExec().isFinished(procId)); 293 294 Procedure<?> result = getProcExec().getResult(procId); 295 assertTrue("Procedure should have failed", result.isFailed()); 296 297 Throwable cause = ProcedureTestingUtility.getExceptionCause(result); 298 assertTrue("Should be UnknownRegionException", cause instanceof UnknownRegionException); 299 } 300 } 301 302 @Test 303 public void testSerializationWithRegionNames() throws Exception { 304 TableName tableName = TableName.valueOf("TestSerialization"); 305 try (Table ignored = UTIL.createTable(tableName, CF)) { 306 307 List<RegionInfo> regions = UTIL.getAdmin().getRegions(tableName); 308 List<byte[]> regionNames = 309 regions.stream().map(RegionInfo::getRegionName).collect(Collectors.toList()); 310 311 ReopenTableRegionsProcedure proc = 312 new ReopenTableRegionsProcedure(tableName, regionNames, 500L, 3); 313 314 long procId = getProcExec().submitProcedure(proc); 315 ProcedureTestingUtility.waitProcedure(getProcExec(), procId); 316 317 assertEquals("TableName should be preserved", tableName, proc.getTableName()); 318 assertEquals("Backoff should be preserved", 500L, proc.getReopenBatchBackoffMillis()); 319 } 320 } 321 322 @Test 323 public void testAllRegionsWithValidNames() throws Exception { 324 TableName tableName = TableName.valueOf("TestAllValidRegions"); 325 try (Table ignored = UTIL.createTable(tableName, CF)) { 326 327 List<RegionInfo> actualRegions = UTIL.getAdmin().getRegions(tableName); 328 assertFalse("Table should have regions", actualRegions.isEmpty()); 329 330 List<byte[]> validRegionNames = 331 actualRegions.stream().map(RegionInfo::getRegionName).collect(Collectors.toList()); 332 333 ReopenTableRegionsProcedure proc = 334 new ReopenTableRegionsProcedure(tableName, validRegionNames, 0L, Integer.MAX_VALUE); 335 336 long procId = getProcExec().submitProcedure(proc); 337 ProcedureTestingUtility.waitProcedure(getProcExec(), procId); 338 339 assertFalse("Procedure should succeed with all valid regions", proc.isFailed()); 340 assertEquals("Should reopen all specified regions", actualRegions.size(), 341 proc.getRegionsReopened()); 342 } 343 } 344 345 @Test 346 public void testSingleInvalidRegion() throws Exception { 347 TableName tableName = TableName.valueOf("TestSingleInvalid"); 348 try (Table ignored = UTIL.createTable(tableName, CF)) { 349 350 List<byte[]> invalidRegionNames = 351 Collections.singletonList(Bytes.toBytes("totally-fake-region")); 352 353 ReopenTableRegionsProcedure proc = 354 new ReopenTableRegionsProcedure(tableName, invalidRegionNames, 0L, Integer.MAX_VALUE); 355 356 long procId = getProcExec().submitProcedure(proc); 357 UTIL.waitFor(60000, proc::isFailed); 358 359 Throwable cause = ProcedureTestingUtility.getExceptionCause(proc); 360 assertTrue("Expected UnknownRegionException", cause instanceof UnknownRegionException); 361 assertTrue("Error message should list the invalid region", 362 cause.getMessage().contains("totally-fake-region")); 363 } 364 } 365 366 @Test 367 public void testRecoveryAfterValidationFailure() throws Exception { 368 TableName tableName = TableName.valueOf("TestRecoveryValidation"); 369 try (Table ignored = UTIL.createTable(tableName, CF)) { 370 371 List<byte[]> invalidRegionNames = 372 Collections.singletonList(Bytes.toBytes("invalid-for-recovery")); 373 374 ReopenTableRegionsProcedure proc = 375 new ReopenTableRegionsProcedure(tableName, invalidRegionNames, 0L, Integer.MAX_VALUE); 376 377 ProcedureExecutor<MasterProcedureEnv> procExec = getProcExec(); 378 long procId = procExec.submitProcedure(proc); 379 380 UTIL.waitFor(60000, () -> procExec.isFinished(procId)); 381 382 Procedure<?> result = procExec.getResult(procId); 383 assertTrue("Procedure should fail validation", result.isFailed()); 384 385 Throwable cause = ProcedureTestingUtility.getExceptionCause(result); 386 assertTrue("Should be UnknownRegionException", cause instanceof UnknownRegionException); 387 assertTrue("Error should mention the invalid region", 388 cause.getMessage().contains("invalid-for-recovery")); 389 } 390 } 391 392 @Test 393 public void testEmptyTableWithNoRegions() throws Exception { 394 TableName tableName = TableName.valueOf("TestEmptyTable"); 395 396 TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName) 397 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(CF)).build(); 398 399 UTIL.getAdmin().createTable(td); 400 401 List<RegionInfo> regions = UTIL.getAdmin().getRegions(tableName); 402 int regionCount = regions.size(); 403 404 ReopenTableRegionsProcedure proc = ReopenTableRegionsProcedure 405 .throttled(UTIL.getConfiguration(), UTIL.getAdmin().getDescriptor(tableName)); 406 407 long procId = getProcExec().submitProcedure(proc); 408 ProcedureTestingUtility.waitProcedure(getProcExec(), procId); 409 410 assertFalse("Procedure should complete successfully even with no regions", proc.isFailed()); 411 assertEquals("Should handle empty table gracefully", regionCount, proc.getRegionsReopened()); 412 } 413 414 @Test 415 public void testConfigChangeDoesNotAffectRunningProcedure() throws Exception { 416 TableName tableName = TableName.valueOf("TestConfigChange"); 417 418 TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName) 419 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(CF)) 420 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_BACKOFF_MILLIS_KEY, "1000") 421 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_SIZE_MAX_KEY, "2").build(); 422 423 UTIL.getAdmin().createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 5); 424 425 ReopenTableRegionsProcedure proc = ReopenTableRegionsProcedure 426 .throttled(UTIL.getConfiguration(), UTIL.getAdmin().getDescriptor(tableName)); 427 428 assertEquals("Initial config should be 1000ms", 1000L, proc.getReopenBatchBackoffMillis()); 429 430 TableDescriptor modifiedTd = TableDescriptorBuilder.newBuilder(td) 431 .setValue(ReopenTableRegionsProcedure.PROGRESSIVE_BATCH_BACKOFF_MILLIS_KEY, "5000").build(); 432 UTIL.getAdmin().modifyTable(modifiedTd); 433 434 assertEquals("Running procedure should keep original config", 1000L, 435 proc.getReopenBatchBackoffMillis()); 436 437 long procId = getProcExec().submitProcedure(proc); 438 ProcedureTestingUtility.waitProcedure(getProcExec(), procId); 439 440 assertFalse("Procedure should complete successfully", proc.isFailed()); 441 } 442}