View Javadoc

1   /**
2    * Copyright The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements. See the NOTICE file distributed with this
6    * work for additional information regarding copyright ownership. The ASF
7    * licenses this file to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   * http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16   * License for the specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.hadoop.hbase.regionserver;
20  
21  import static org.junit.Assert.assertEquals;
22  import static org.junit.Assert.assertFalse;
23  import static org.junit.Assert.assertTrue;
24  import static org.mockito.Mockito.*;
25  
26  import java.io.IOException;
27  import java.util.ArrayList;
28  import java.util.List;
29  
30  import org.apache.hadoop.conf.Configuration;
31  import org.apache.hadoop.fs.FileSystem;
32  import org.apache.hadoop.fs.Path;
33  import org.apache.hadoop.hbase.Cell;
34  import org.apache.hadoop.hbase.CoordinatedStateManager;
35  import org.apache.hadoop.hbase.CoordinatedStateManagerFactory;
36  import org.apache.hadoop.hbase.HBaseTestingUtility;
37  import org.apache.hadoop.hbase.HColumnDescriptor;
38  import org.apache.hadoop.hbase.HConstants;
39  import org.apache.hadoop.hbase.HRegionInfo;
40  import org.apache.hadoop.hbase.HTableDescriptor;
41  import org.apache.hadoop.hbase.Server;
42  import org.apache.hadoop.hbase.TableName;
43  import org.apache.hadoop.hbase.client.Durability;
44  import org.apache.hadoop.hbase.client.Put;
45  import org.apache.hadoop.hbase.client.Scan;
46  import org.apache.hadoop.hbase.testclassification.SmallTests;
47  import org.apache.hadoop.hbase.util.Bytes;
48  import org.apache.hadoop.hbase.util.FSUtils;
49  import org.apache.hadoop.hbase.wal.WALFactory;
50  import org.apache.zookeeper.KeeperException;
51  import org.junit.After;
52  import org.junit.Before;
53  import org.junit.Test;
54  import org.junit.experimental.categories.Category;
55  import org.mockito.Mockito;
56  
57  import com.google.common.collect.ImmutableList;
58  
59  /**
60   * Test the {@link RegionMergeTransactionImpl} class against two HRegions (as
61   * opposed to running cluster).
62   */
63  @Category(SmallTests.class)
64  public class TestRegionMergeTransaction {
65    private final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
66    private final Path testdir = TEST_UTIL.getDataTestDir(this.getClass()
67        .getName());
68    private HRegion region_a;
69    private HRegion region_b;
70    private HRegion region_c;
71    private WALFactory wals;
72    private FileSystem fs;
73    // Start rows of region_a,region_b,region_c
74    private static final byte[] STARTROW_A = new byte[] { 'a', 'a', 'a' };
75    private static final byte[] STARTROW_B = new byte[] { 'g', 'g', 'g' };
76    private static final byte[] STARTROW_C = new byte[] { 'w', 'w', 'w' };
77    private static final byte[] ENDROW = new byte[] { '{', '{', '{' };
78    private static final byte[] CF = HConstants.CATALOG_FAMILY;
79  
80    @Before
81    public void setup() throws IOException {
82      this.fs = FileSystem.get(TEST_UTIL.getConfiguration());
83      this.fs.delete(this.testdir, true);
84      final Configuration walConf = new Configuration(TEST_UTIL.getConfiguration());
85      FSUtils.setRootDir(walConf, this.testdir);
86      this.wals = new WALFactory(walConf, null, TestRegionMergeTransaction.class.getName());
87      this.region_a = createRegion(this.testdir, this.wals, STARTROW_A, STARTROW_B);
88      this.region_b = createRegion(this.testdir, this.wals, STARTROW_B, STARTROW_C);
89      this.region_c = createRegion(this.testdir, this.wals, STARTROW_C, ENDROW);
90      assert region_a != null && region_b != null && region_c != null;
91      TEST_UTIL.getConfiguration().setBoolean("hbase.testing.nocluster", true);
92    }
93  
94    @After
95    public void teardown() throws IOException {
96      for (HRegion region : new HRegion[] { region_a, region_b, region_c }) {
97        if (region != null && !region.isClosed()) region.close();
98        if (region != null && this.fs.exists(region.getRegionFileSystem().getRegionDir())
99            && !this.fs.delete(region.getRegionFileSystem().getRegionDir(), true)) {
100         throw new IOException("Failed deleting of "
101             + region.getRegionFileSystem().getRegionDir());
102       }
103     }
104     if (this.wals != null) {
105       this.wals.close();
106     }
107     this.fs.delete(this.testdir, true);
108   }
109 
110   /**
111    * Test straight prepare works. Tries to merge on {@link #region_a} and
112    * {@link #region_b}
113    * @throws IOException
114    */
115   @Test
116   public void testPrepare() throws IOException {
117     prepareOnGoodRegions();
118   }
119 
120   private RegionMergeTransactionImpl prepareOnGoodRegions() throws IOException {
121     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(region_a, region_b,
122         false);
123     RegionMergeTransactionImpl spyMT = Mockito.spy(mt);
124     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
125         region_a.getRegionInfo().getRegionName());
126     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
127         region_b.getRegionInfo().getRegionName());
128     assertTrue(spyMT.prepare(null));
129     return spyMT;
130   }
131 
132   /**
133    * Test merging the same region
134    */
135   @Test
136   public void testPrepareWithSameRegion() throws IOException {
137     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(this.region_a,
138         this.region_a, true);
139     assertFalse("should not merge the same region even if it is forcible ",
140         mt.prepare(null));
141   }
142 
143   /**
144    * Test merging two not adjacent regions under a common merge
145    */
146   @Test
147   public void testPrepareWithRegionsNotAdjacent() throws IOException {
148     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(this.region_a,
149         this.region_c, false);
150     assertFalse("should not merge two regions if they are adjacent except it is forcible",
151         mt.prepare(null));
152   }
153 
154   /**
155    * Test merging two not adjacent regions under a compulsory merge
156    */
157   @Test
158   public void testPrepareWithRegionsNotAdjacentUnderCompulsory()
159       throws IOException {
160     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(region_a, region_c,
161         true);
162     RegionMergeTransactionImpl spyMT = Mockito.spy(mt);
163     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
164         region_a.getRegionInfo().getRegionName());
165     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
166         region_c.getRegionInfo().getRegionName());
167     assertTrue("Since focible is true, should merge two regions even if they are not adjacent",
168         spyMT.prepare(null));
169   }
170 
171   /**
172    * Pass a reference store
173    */
174   @Test
175   public void testPrepareWithRegionsWithReference() throws IOException {
176     HStore storeMock = Mockito.mock(HStore.class);
177     when(storeMock.hasReferences()).thenReturn(true);
178     when(storeMock.getFamily()).thenReturn(new HColumnDescriptor("cf"));
179     when(storeMock.close()).thenReturn(ImmutableList.<StoreFile>of());
180     this.region_a.stores.put(Bytes.toBytes(""), storeMock);
181     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(this.region_a,
182         this.region_b, false);
183     assertFalse(
184         "a region should not be mergeable if it has instances of store file references",
185         mt.prepare(null));
186   }
187 
188   @Test
189   public void testPrepareWithClosedRegion() throws IOException {
190     this.region_a.close();
191     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(this.region_a,
192         this.region_b, false);
193     assertFalse(mt.prepare(null));
194   }
195 
196   /**
197    * Test merging regions which are merged regions and has reference in hbase:meta all
198    * the same
199    */
200   @Test
201   public void testPrepareWithRegionsWithMergeReference() throws IOException {
202     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(region_a, region_b,
203         false);
204     RegionMergeTransactionImpl spyMT = Mockito.spy(mt);
205     doReturn(true).when(spyMT).hasMergeQualifierInMeta(null,
206         region_a.getRegionInfo().getRegionName());
207     doReturn(true).when(spyMT).hasMergeQualifierInMeta(null,
208         region_b.getRegionInfo().getRegionName());
209     assertFalse(spyMT.prepare(null));
210   }
211 
212   /**
213    * Test RegionMergeTransactionListener
214    */
215   @Test public void testRegionMergeTransactionListener() throws Exception {
216     RegionMergeTransactionImpl mt = new RegionMergeTransactionImpl(region_a, region_b,
217         false);
218     RegionMergeTransactionImpl spyMT = Mockito.spy(mt);
219     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
220         region_a.getRegionInfo().getRegionName());
221     doReturn(false).when(spyMT).hasMergeQualifierInMeta(null,
222         region_b.getRegionInfo().getRegionName());
223     RegionMergeTransaction.TransactionListener listener =
224             Mockito.mock(RegionMergeTransaction.TransactionListener.class);
225     mt.registerTransactionListener(listener);
226     mt.prepare(null);
227     TEST_UTIL.getConfiguration().setInt(HConstants.REGIONSERVER_PORT, 0);
228     CoordinatedStateManager cp = CoordinatedStateManagerFactory.getCoordinatedStateManager(
229       TEST_UTIL.getConfiguration());
230     Server mockServer = new HRegionServer(TEST_UTIL.getConfiguration(), cp);
231     mt.execute(mockServer, null);
232     verify(listener).transition(mt,
233             RegionMergeTransaction.RegionMergeTransactionPhase.STARTED,
234             RegionMergeTransaction.RegionMergeTransactionPhase.PREPARED);
235     verify(listener, times(10)).transition(any(RegionMergeTransaction.class),
236             any(RegionMergeTransaction.RegionMergeTransactionPhase.class),
237             any(RegionMergeTransaction.RegionMergeTransactionPhase.class));
238     verifyNoMoreInteractions(listener);
239   }
240 
241   @Test
242   public void testWholesomeMerge() throws IOException, InterruptedException {
243     final int rowCountOfRegionA = loadRegion(this.region_a, CF, true);
244     final int rowCountOfRegionB = loadRegion(this.region_b, CF, true);
245     assertTrue(rowCountOfRegionA > 0 && rowCountOfRegionB > 0);
246     assertEquals(rowCountOfRegionA, countRows(this.region_a));
247     assertEquals(rowCountOfRegionB, countRows(this.region_b));
248 
249     // Start transaction.
250     RegionMergeTransactionImpl mt = prepareOnGoodRegions();
251 
252     // Run the execute. Look at what it returns.
253     TEST_UTIL.getConfiguration().setInt(HConstants.REGIONSERVER_PORT, 0);
254     CoordinatedStateManager cp = CoordinatedStateManagerFactory.getCoordinatedStateManager(
255       TEST_UTIL.getConfiguration());
256     Server mockServer = new HRegionServer(TEST_UTIL.getConfiguration(), cp);
257     HRegion mergedRegion = (HRegion)mt.execute(mockServer, null);
258     // Do some assertions about execution.
259     assertTrue(this.fs.exists(mt.getMergesDir()));
260     // Assert region_a and region_b is closed.
261     assertTrue(region_a.isClosed());
262     assertTrue(region_b.isClosed());
263 
264     // Assert mergedir is empty -- because its content will have been moved out
265     // to be under the merged region dirs.
266     assertEquals(0, this.fs.listStatus(mt.getMergesDir()).length);
267     // Check merged region have correct key span.
268     assertTrue(Bytes.equals(this.region_a.getRegionInfo().getStartKey(),
269         mergedRegion.getRegionInfo().getStartKey()));
270     assertTrue(Bytes.equals(this.region_b.getRegionInfo().getEndKey(),
271         mergedRegion.getRegionInfo().getEndKey()));
272     // Count rows. merged region are already open
273     try {
274       int mergedRegionRowCount = countRows(mergedRegion);
275       assertEquals((rowCountOfRegionA + rowCountOfRegionB),
276           mergedRegionRowCount);
277     } finally {
278       HRegion.closeHRegion(mergedRegion);
279     }
280     // Assert the write lock is no longer held on region_a and region_b
281     assertTrue(!this.region_a.lock.writeLock().isHeldByCurrentThread());
282     assertTrue(!this.region_b.lock.writeLock().isHeldByCurrentThread());
283   }
284 
285   @Test
286   public void testRollback() throws IOException, InterruptedException {
287     final int rowCountOfRegionA = loadRegion(this.region_a, CF, true);
288     final int rowCountOfRegionB = loadRegion(this.region_b, CF, true);
289     assertTrue(rowCountOfRegionA > 0 && rowCountOfRegionB > 0);
290     assertEquals(rowCountOfRegionA, countRows(this.region_a));
291     assertEquals(rowCountOfRegionB, countRows(this.region_b));
292 
293     // Start transaction.
294     RegionMergeTransactionImpl mt = prepareOnGoodRegions();
295 
296     when(mt.createMergedRegionFromMerges(region_a, region_b,
297         mt.getMergedRegionInfo())).thenThrow(
298         new MockedFailedMergedRegionCreation());
299 
300     // Run the execute. Look at what it returns.
301     boolean expectedException = false;
302     TEST_UTIL.getConfiguration().setInt(HConstants.REGIONSERVER_PORT, 0);
303     CoordinatedStateManager cp = CoordinatedStateManagerFactory.getCoordinatedStateManager(
304       TEST_UTIL.getConfiguration());
305     Server mockServer = new HRegionServer(TEST_UTIL.getConfiguration(), cp);
306     try {
307       mt.execute(mockServer, null);
308     } catch (MockedFailedMergedRegionCreation e) {
309       expectedException = true;
310     }
311     assertTrue(expectedException);
312     // Run rollback
313     assertTrue(mt.rollback(null, null));
314 
315     // Assert I can scan region_a and region_b.
316     int rowCountOfRegionA2 = countRows(this.region_a);
317     assertEquals(rowCountOfRegionA, rowCountOfRegionA2);
318     int rowCountOfRegionB2 = countRows(this.region_b);
319     assertEquals(rowCountOfRegionB, rowCountOfRegionB2);
320 
321     // Assert rollback cleaned up stuff in fs
322     assertTrue(!this.fs.exists(HRegion.getRegionDir(this.testdir,
323         mt.getMergedRegionInfo())));
324 
325     assertTrue(!this.region_a.lock.writeLock().isHeldByCurrentThread());
326     assertTrue(!this.region_b.lock.writeLock().isHeldByCurrentThread());
327 
328     // Now retry the merge but do not throw an exception this time.
329     assertTrue(mt.prepare(null));
330     HRegion mergedRegion = (HRegion)mt.execute(mockServer, null);
331     // Count rows. daughters are already open
332     // Count rows. merged region are already open
333     try {
334       int mergedRegionRowCount = countRows(mergedRegion);
335       assertEquals((rowCountOfRegionA + rowCountOfRegionB),
336           mergedRegionRowCount);
337     } finally {
338       HRegion.closeHRegion(mergedRegion);
339     }
340     // Assert the write lock is no longer held on region_a and region_b
341     assertTrue(!this.region_a.lock.writeLock().isHeldByCurrentThread());
342     assertTrue(!this.region_b.lock.writeLock().isHeldByCurrentThread());
343   }
344 
345   @Test
346   public void testFailAfterPONR() throws IOException, KeeperException, InterruptedException {
347     final int rowCountOfRegionA = loadRegion(this.region_a, CF, true);
348     final int rowCountOfRegionB = loadRegion(this.region_b, CF, true);
349     assertTrue(rowCountOfRegionA > 0 && rowCountOfRegionB > 0);
350     assertEquals(rowCountOfRegionA, countRows(this.region_a));
351     assertEquals(rowCountOfRegionB, countRows(this.region_b));
352 
353     // Start transaction.
354     RegionMergeTransactionImpl mt = prepareOnGoodRegions();
355     Mockito.doThrow(new MockedFailedMergedRegionOpen())
356         .when(mt)
357         .openMergedRegion((Server) Mockito.anyObject(),
358             (RegionServerServices) Mockito.anyObject(),
359             (HRegion) Mockito.anyObject());
360 
361     // Run the execute. Look at what it returns.
362     boolean expectedException = false;
363     TEST_UTIL.getConfiguration().setInt(HConstants.REGIONSERVER_PORT, 0);
364     CoordinatedStateManager cp = CoordinatedStateManagerFactory.getCoordinatedStateManager(
365       TEST_UTIL.getConfiguration());
366     Server mockServer = new HRegionServer(TEST_UTIL.getConfiguration(), cp);
367     try {
368       mt.execute(mockServer, null);
369     } catch (MockedFailedMergedRegionOpen e) {
370       expectedException = true;
371     }
372     assertTrue(expectedException);
373     // Run rollback returns false that we should restart.
374     assertFalse(mt.rollback(null, null));
375     // Make sure that merged region is still in the filesystem, that
376     // they have not been removed; this is supposed to be the case if we go
377     // past point of no return.
378     Path tableDir = this.region_a.getRegionFileSystem().getRegionDir()
379         .getParent();
380     Path mergedRegionDir = new Path(tableDir, mt.getMergedRegionInfo()
381         .getEncodedName());
382     assertTrue(TEST_UTIL.getTestFileSystem().exists(mergedRegionDir));
383   }
384 
385   @Test
386   public void testMergedRegionBoundary() {
387     TableName tableName =
388         TableName.valueOf("testMergedRegionBoundary");
389     byte[] a = Bytes.toBytes("a");
390     byte[] b = Bytes.toBytes("b");
391     byte[] z = Bytes.toBytes("z");
392     HRegionInfo r1 = new HRegionInfo(tableName);
393     HRegionInfo r2 = new HRegionInfo(tableName, a, z);
394     HRegionInfo m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
395     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
396         && Bytes.equals(m.getEndKey(), r1.getEndKey()));
397 
398     r1 = new HRegionInfo(tableName, null, a);
399     r2 = new HRegionInfo(tableName, a, z);
400     m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
401     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
402         && Bytes.equals(m.getEndKey(), r2.getEndKey()));
403 
404     r1 = new HRegionInfo(tableName, null, a);
405     r2 = new HRegionInfo(tableName, z, null);
406     m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
407     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
408         && Bytes.equals(m.getEndKey(), r2.getEndKey()));
409 
410     r1 = new HRegionInfo(tableName, a, z);
411     r2 = new HRegionInfo(tableName, z, null);
412     m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
413     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
414       && Bytes.equals(m.getEndKey(), r2.getEndKey()));
415 
416     r1 = new HRegionInfo(tableName, a, b);
417     r2 = new HRegionInfo(tableName, b, z);
418     m = RegionMergeTransactionImpl.getMergedRegionInfo(r1, r2);
419     assertTrue(Bytes.equals(m.getStartKey(), r1.getStartKey())
420       && Bytes.equals(m.getEndKey(), r2.getEndKey()));
421   }
422 
423   /**
424    * Exception used in this class only.
425    */
426   @SuppressWarnings("serial")
427   private class MockedFailedMergedRegionCreation extends IOException {
428   }
429 
430   @SuppressWarnings("serial")
431   private class MockedFailedMergedRegionOpen extends IOException {
432   }
433 
434   private HRegion createRegion(final Path testdir, final WALFactory wals,
435       final byte[] startrow, final byte[] endrow)
436       throws IOException {
437     // Make a region with start and end keys.
438     HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("table"));
439     HColumnDescriptor hcd = new HColumnDescriptor(CF);
440     htd.addFamily(hcd);
441     HRegionInfo hri = new HRegionInfo(htd.getTableName(), startrow, endrow);
442     HRegion a = HRegion.createHRegion(hri, testdir,
443         TEST_UTIL.getConfiguration(), htd);
444     HRegion.closeHRegion(a);
445     return HRegion.openHRegion(testdir, hri, htd,
446       wals.getWAL(hri.getEncodedNameAsBytes(), hri.getTable().getNamespace()),
447       TEST_UTIL.getConfiguration());
448   }
449 
450   private int countRows(final HRegion r) throws IOException {
451     int rowcount = 0;
452     InternalScanner scanner = r.getScanner(new Scan());
453     try {
454       List<Cell> kvs = new ArrayList<Cell>();
455       boolean hasNext = true;
456       while (hasNext) {
457         hasNext = scanner.next(kvs);
458         if (!kvs.isEmpty())
459           rowcount++;
460       }
461     } finally {
462       scanner.close();
463     }
464     return rowcount;
465   }
466 
467   /**
468    * Load region with rows from 'aaa' to 'zzz', skip the rows which are out of
469    * range of the region
470    * @param r Region
471    * @param f Family
472    * @param flush flush the cache if true
473    * @return Count of rows loaded.
474    * @throws IOException
475    */
476   private int loadRegion(final HRegion r, final byte[] f, final boolean flush)
477       throws IOException {
478     byte[] k = new byte[3];
479     int rowCount = 0;
480     for (byte b1 = 'a'; b1 <= 'z'; b1++) {
481       for (byte b2 = 'a'; b2 <= 'z'; b2++) {
482         for (byte b3 = 'a'; b3 <= 'z'; b3++) {
483           k[0] = b1;
484           k[1] = b2;
485           k[2] = b3;
486           if (!HRegion.rowIsInRange(r.getRegionInfo(), k)) {
487             continue;
488           }
489           Put put = new Put(k);
490           put.add(f, null, k);
491           if (r.getWAL() == null)
492             put.setDurability(Durability.SKIP_WAL);
493           r.put(put);
494           rowCount++;
495         }
496       }
497       if (flush) {
498         r.flush(true);
499       }
500     }
501     return rowCount;
502   }
503 
504 }