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}