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.assertNotEquals;
022import static org.junit.jupiter.api.Assertions.assertThrows;
023
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.hbase.HBaseTestingUtil;
032import org.apache.hadoop.hbase.TableName;
033import org.apache.hadoop.hbase.TableNotFoundException;
034import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
035import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
036import org.apache.hadoop.hbase.client.RegionInfo;
037import org.apache.hadoop.hbase.client.TableDescriptor;
038import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
039import org.apache.hadoop.hbase.master.HMaster;
040import org.apache.hadoop.hbase.regionserver.HRegion;
041import org.apache.hadoop.hbase.regionserver.MetricsRegionWrapperImpl;
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 TestReopenTableRegionsIntegration {
053
054  private static final HBaseTestingUtil UTIL = new HBaseTestingUtil();
055  private static final TableName TABLE_NAME = TableName.valueOf("testLazyUpdateReopen");
056  private static final byte[] CF = Bytes.toBytes("cf");
057
058  @BeforeAll
059  public static void setupCluster() throws Exception {
060    Configuration conf = UTIL.getConfiguration();
061    conf.setInt(MasterProcedureConstants.MASTER_PROCEDURE_THREADS, 1);
062    UTIL.startMiniCluster(1);
063  }
064
065  @AfterAll
066  public static void tearDown() throws Exception {
067    UTIL.shutdownMiniCluster();
068  }
069
070  @Test
071  public void testLazyUpdateThenReopenUpdatesTableDescriptorHash() throws Exception {
072    // Step 1: Create table with column family and 3 regions
073    ColumnFamilyDescriptor cfd =
074      ColumnFamilyDescriptorBuilder.newBuilder(CF).setMaxVersions(1).build();
075
076    TableDescriptor td = TableDescriptorBuilder.newBuilder(TABLE_NAME).setColumnFamily(cfd)
077      .setMaxFileSize(100 * 1024 * 1024L).build();
078
079    UTIL.getAdmin().createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 3);
080    UTIL.waitTableAvailable(TABLE_NAME);
081
082    try {
083      // Step 2: Capture initial tableDescriptorHash from all regions
084      List<HRegion> regions = UTIL.getHBaseCluster().getRegions(TABLE_NAME);
085      assertEquals(3, regions.size(), "Expected 3 regions");
086
087      Map<byte[], String> initialHashes = new HashMap<>();
088
089      for (HRegion region : regions) {
090        String hash;
091        try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
092          hash = wrapper.getTableDescriptorHash();
093        }
094        initialHashes.put(region.getRegionInfo().getRegionName(), hash);
095      }
096
097      // Verify all regions have same hash
098      Set<String> uniqueHashes = new HashSet<>(initialHashes.values());
099      assertEquals(1, uniqueHashes.size(), "All regions should have same hash");
100      String initialHash = uniqueHashes.iterator().next();
101
102      // Step 3: Perform lazy table descriptor update
103      ColumnFamilyDescriptor newCfd =
104        ColumnFamilyDescriptorBuilder.newBuilder(cfd).setMaxVersions(5).build();
105
106      TableDescriptor newTd = TableDescriptorBuilder.newBuilder(td).modifyColumnFamily(newCfd)
107        .setMaxFileSize(200 * 1024 * 1024L).build();
108
109      // Perform lazy update (reopenRegions = false)
110      UTIL.getAdmin().modifyTableAsync(newTd, false).get();
111
112      // Wait for modification to complete
113      UTIL.waitFor(30000, () -> {
114        try {
115          TableDescriptor currentTd = UTIL.getAdmin().getDescriptor(TABLE_NAME);
116          return currentTd.getMaxFileSize() == 200 * 1024 * 1024L;
117        } catch (Exception e) {
118          return false;
119        }
120      });
121
122      // Step 4: Verify tableDescriptorHash has NOT changed in region metrics
123      List<HRegion> regionsAfterLazyUpdate = UTIL.getHBaseCluster().getRegions(TABLE_NAME);
124      for (HRegion region : regionsAfterLazyUpdate) {
125        String currentHash;
126        try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
127          currentHash = wrapper.getTableDescriptorHash();
128        }
129        assertEquals(initialHashes.get(region.getRegionInfo().getRegionName()), currentHash,
130          "Hash should NOT change without region reopen");
131      }
132
133      // Verify the table descriptor itself has changed
134      TableDescriptor currentTd = UTIL.getAdmin().getDescriptor(TABLE_NAME);
135      String newDescriptorHash = currentTd.getDescriptorHash();
136      assertNotEquals(initialHash, newDescriptorHash, "Table descriptor should have new hash");
137
138      // Step 5: Use new Admin API to reopen all regions
139      UTIL.getAdmin().reopenTableRegions(TABLE_NAME);
140
141      // Wait for all regions to be reopened
142      UTIL.waitFor(60000, () -> {
143        try {
144          List<HRegion> currentRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME);
145          if (currentRegions.size() != 3) {
146            return false;
147          }
148
149          // Check if all regions now have the new hash
150          for (HRegion region : currentRegions) {
151            String hash;
152            try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
153              hash = wrapper.getTableDescriptorHash();
154            }
155            if (hash.equals(initialHash)) {
156              return false;
157            }
158          }
159          return true;
160        } catch (Exception e) {
161          return false;
162        }
163      });
164
165      // Step 6: Verify tableDescriptorHash HAS changed in all region metrics
166      List<HRegion> reopenedRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME);
167      assertEquals(3, reopenedRegions.size(), "Should still have 3 regions");
168
169      for (HRegion region : reopenedRegions) {
170        String currentHash;
171        try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
172          currentHash = wrapper.getTableDescriptorHash();
173        }
174        assertNotEquals(initialHash, currentHash, "Hash SHOULD change after region reopen");
175        assertEquals(newDescriptorHash, currentHash, "Hash should match current table descriptor");
176      }
177
178      // Verify all regions show the same new hash
179      Set<String> newHashes = new HashSet<>();
180      for (HRegion region : reopenedRegions) {
181        try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
182          newHashes.add(wrapper.getTableDescriptorHash());
183        }
184      }
185      assertEquals(1, newHashes.size(), "All regions should have same new hash");
186
187    } finally {
188      UTIL.deleteTable(TABLE_NAME);
189    }
190  }
191
192  @Test
193  public void testLazyUpdateThenReopenSpecificRegions() throws Exception {
194    TableName tableName = TableName.valueOf("testSpecificRegionsReopen");
195
196    // Step 1: Create table with 5 regions
197    ColumnFamilyDescriptor cfd =
198      ColumnFamilyDescriptorBuilder.newBuilder(CF).setMaxVersions(1).build();
199
200    TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName).setColumnFamily(cfd)
201      .setMaxFileSize(100 * 1024 * 1024L).build();
202
203    UTIL.getAdmin().createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 5);
204    UTIL.waitTableAvailable(tableName);
205
206    try {
207      // Step 2: Capture initial hashes
208      List<HRegion> regions = UTIL.getHBaseCluster().getRegions(tableName);
209      assertEquals(5, regions.size(), "Expected 5 regions");
210
211      Map<byte[], String> initialHashes = new HashMap<>();
212
213      for (HRegion region : regions) {
214        String hash;
215        try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
216          hash = wrapper.getTableDescriptorHash();
217        }
218        initialHashes.put(region.getRegionInfo().getRegionName(), hash);
219      }
220
221      String initialHash = initialHashes.values().iterator().next();
222
223      // Step 3: Perform lazy update
224      ColumnFamilyDescriptor newCfd =
225        ColumnFamilyDescriptorBuilder.newBuilder(cfd).setMaxVersions(10).build();
226
227      TableDescriptor newTd = TableDescriptorBuilder.newBuilder(td).modifyColumnFamily(newCfd)
228        .setMaxFileSize(300 * 1024 * 1024L).build();
229
230      UTIL.getAdmin().modifyTableAsync(newTd, false).get();
231
232      UTIL.waitFor(30000, () -> {
233        try {
234          TableDescriptor currentTd = UTIL.getAdmin().getDescriptor(tableName);
235          return currentTd.getMaxFileSize() == 300 * 1024 * 1024L;
236        } catch (Exception e) {
237          return false;
238        }
239      });
240
241      String newDescriptorHash = UTIL.getAdmin().getDescriptor(tableName).getDescriptorHash();
242
243      // Step 4: Reopen only first 2 regions
244      List<RegionInfo> regionsToReopen = new ArrayList<>();
245      regionsToReopen.add(regions.get(0).getRegionInfo());
246      regionsToReopen.add(regions.get(1).getRegionInfo());
247
248      UTIL.getAdmin().reopenTableRegions(tableName, regionsToReopen);
249
250      // Wait for those regions to reopen
251      UTIL.waitFor(60000, () -> {
252        try {
253          List<HRegion> currentRegions = UTIL.getHBaseCluster().getRegions(tableName);
254          int newHashCount = 0;
255          for (HRegion region : currentRegions) {
256            String hash;
257            try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
258              hash = wrapper.getTableDescriptorHash();
259            }
260            if (!hash.equals(initialHash)) {
261              newHashCount++;
262            }
263          }
264          return newHashCount >= 2;
265        } catch (Exception e) {
266          return false;
267        }
268      });
269
270      // Step 5: Verify only reopened regions have new hash
271      List<HRegion> regionsAfterFirstReopen = UTIL.getHBaseCluster().getRegions(tableName);
272      int newHashCount = 0;
273      int oldHashCount = 0;
274
275      for (HRegion region : regionsAfterFirstReopen) {
276        String currentHash;
277        try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
278          currentHash = wrapper.getTableDescriptorHash();
279        }
280
281        if (currentHash.equals(newDescriptorHash)) {
282          newHashCount++;
283        } else if (currentHash.equals(initialHash)) {
284          oldHashCount++;
285        }
286      }
287
288      assertEquals(2, newHashCount, "Should have 2 regions with new hash");
289      assertEquals(3, oldHashCount, "Should have 3 regions with old hash");
290
291      // Step 6: Reopen remaining regions
292      List<RegionInfo> remainingRegions = new ArrayList<>();
293      for (int i = 2; i < regions.size(); i++) {
294        remainingRegions.add(regions.get(i).getRegionInfo());
295      }
296
297      UTIL.getAdmin().reopenTableRegions(tableName, remainingRegions);
298
299      // Wait for all regions to have new hash
300      UTIL.waitFor(60000, () -> {
301        try {
302          List<HRegion> currentRegions = UTIL.getHBaseCluster().getRegions(tableName);
303          for (HRegion region : currentRegions) {
304            String hash;
305            try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
306              hash = wrapper.getTableDescriptorHash();
307            }
308            if (!hash.equals(newDescriptorHash)) {
309              return false;
310            }
311          }
312          return true;
313        } catch (Exception e) {
314          return false;
315        }
316      });
317
318      // Step 7: Verify all regions now have new hash
319      List<HRegion> finalRegions = UTIL.getHBaseCluster().getRegions(tableName);
320      for (HRegion region : finalRegions) {
321        String currentHash;
322        try (MetricsRegionWrapperImpl wrapper = new MetricsRegionWrapperImpl(region)) {
323          currentHash = wrapper.getTableDescriptorHash();
324        }
325
326        assertEquals(newDescriptorHash, currentHash, "All regions should now have new hash");
327      }
328
329    } finally {
330      UTIL.deleteTable(tableName);
331    }
332  }
333
334  @Test
335  public void testReopenTableRegionsFailsCleanlyWhenDescriptorMissing() throws Exception {
336    TableName tableName = TableName.valueOf("testReopenTableRegionsFailsWhenDescriptorMissing");
337    TableDescriptor td = TableDescriptorBuilder.newBuilder(tableName)
338      .setColumnFamily(ColumnFamilyDescriptorBuilder.of(CF)).build();
339    UTIL.getAdmin().createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 3);
340    UTIL.waitTableAvailable(tableName);
341
342    HMaster master = UTIL.getMiniHBaseCluster().getMaster();
343    TableDescriptor removedDescriptor = null;
344    try {
345      removedDescriptor = master.getTableDescriptors().remove(tableName);
346      assertThrows(TableNotFoundException.class,
347        () -> UTIL.getAdmin().reopenTableRegions(tableName));
348    } finally {
349      if (removedDescriptor != null && master.getTableDescriptors().get(tableName) == null) {
350        master.getTableDescriptors().update(removedDescriptor);
351      }
352      UTIL.deleteTable(tableName);
353    }
354  }
355}