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