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}