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