1   /**
2    * Copyright 2010 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  package org.apache.hadoop.hbase.regionserver;
21  
22  import static org.junit.Assert.assertEquals;
23  import static org.junit.Assert.assertFalse;
24  import static org.junit.Assert.assertTrue;
25  import static org.mockito.Mockito.spy;
26  import static org.mockito.Mockito.when;
27  
28  import java.io.IOException;
29  import java.util.ArrayList;
30  import java.util.List;
31  
32  import org.apache.hadoop.fs.FileSystem;
33  import org.apache.hadoop.fs.Path;
34  import org.apache.hadoop.hbase.HBaseTestingUtility;
35  import org.apache.hadoop.hbase.HColumnDescriptor;
36  import org.apache.hadoop.hbase.HConstants;
37  import org.apache.hadoop.hbase.HRegionInfo;
38  import org.apache.hadoop.hbase.HTableDescriptor;
39  import org.apache.hadoop.hbase.KeyValue;
40  import org.apache.hadoop.hbase.Server;
41  import org.apache.hadoop.hbase.SmallTests;
42  import org.apache.hadoop.hbase.client.Scan;
43  import org.apache.hadoop.hbase.io.hfile.CacheConfig;
44  import org.apache.hadoop.hbase.io.hfile.HFile;
45  import org.apache.hadoop.hbase.io.hfile.LruBlockCache;
46  import org.apache.hadoop.hbase.regionserver.wal.HLog;
47  import org.apache.hadoop.hbase.util.Bytes;
48  import org.apache.hadoop.hbase.util.PairOfSameType;
49  import org.apache.zookeeper.KeeperException;
50  import org.junit.After;
51  import org.junit.Before;
52  import org.junit.Test;
53  import org.junit.experimental.categories.Category;
54  import org.mockito.Mockito;
55  
56  import com.google.common.collect.ImmutableList;
57  
58  /**
59   * Test the {@link SplitTransaction} class against an HRegion (as opposed to
60   * running cluster).
61   */
62  @Category(SmallTests.class)
63  public class TestSplitTransaction {
64    private final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
65    private final Path testdir =
66      TEST_UTIL.getDataTestDir(this.getClass().getName());
67    private HRegion parent;
68    private HLog wal;
69    private FileSystem fs;
70    private static final byte [] STARTROW = new byte [] {'a', 'a', 'a'};
71    // '{' is next ascii after 'z'.
72    private static final byte [] ENDROW = new byte [] {'{', '{', '{'};
73    private static final byte [] GOOD_SPLIT_ROW = new byte [] {'d', 'd', 'd'};
74    private static final byte [] CF = HConstants.CATALOG_FAMILY;
75    
76    @Before public void setup() throws IOException {
77      this.fs = FileSystem.get(TEST_UTIL.getConfiguration());
78      this.fs.delete(this.testdir, true);
79      this.wal = new HLog(fs, new Path(this.testdir, "logs"),
80        new Path(this.testdir, "archive"),
81        TEST_UTIL.getConfiguration());
82      this.parent = createRegion(this.testdir, this.wal);
83      TEST_UTIL.getConfiguration().setBoolean("hbase.testing.nocluster", true);
84    }
85  
86    @After public void teardown() throws IOException {
87      if (this.parent != null && !this.parent.isClosed()) this.parent.close();
88      if (this.fs.exists(this.parent.getRegionDir()) &&
89          !this.fs.delete(this.parent.getRegionDir(), true)) {
90        throw new IOException("Failed delete of " + this.parent.getRegionDir());
91      }
92      if (this.wal != null) this.wal.closeAndDelete();
93      this.fs.delete(this.testdir, true);
94    }
95  
96    @Test public void testFailAfterPONR() throws IOException, KeeperException {
97      final int rowcount = TEST_UTIL.loadRegion(this.parent, CF);
98      assertTrue(rowcount > 0);
99      int parentRowCount = countRows(this.parent);
100     assertEquals(rowcount, parentRowCount);
101 
102     // Start transaction.
103     SplitTransaction st = prepareGOOD_SPLIT_ROW();
104     SplitTransaction spiedUponSt = spy(st);
105     Mockito
106         .doThrow(new MockedFailedDaughterOpen())
107         .when(spiedUponSt)
108         .openDaughterRegion((Server) Mockito.anyObject(),
109             (HRegion) Mockito.anyObject());
110 
111     // Run the execute.  Look at what it returns.
112     boolean expectedException = false;
113     Server mockServer = Mockito.mock(Server.class);
114     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
115     try {
116       spiedUponSt.execute(mockServer, null);
117     } catch (IOException e) {
118       if (e.getCause() != null &&
119           e.getCause() instanceof MockedFailedDaughterOpen) {
120         expectedException = true;
121       }
122     }
123     assertTrue(expectedException);
124     // Run rollback returns that we should restart.
125     assertFalse(spiedUponSt.rollback(null, null));
126     // Make sure that region a and region b are still in the filesystem, that
127     // they have not been removed; this is supposed to be the case if we go
128     // past point of no return.
129     Path tableDir =  this.parent.getRegionDir().getParent();
130     Path daughterADir =
131       new Path(tableDir, spiedUponSt.getFirstDaughter().getEncodedName());
132     Path daughterBDir =
133       new Path(tableDir, spiedUponSt.getSecondDaughter().getEncodedName());
134     assertTrue(TEST_UTIL.getTestFileSystem().exists(daughterADir));
135     assertTrue(TEST_UTIL.getTestFileSystem().exists(daughterBDir));
136   }
137 
138   /**
139    * Test straight prepare works.  Tries to split on {@link #GOOD_SPLIT_ROW}
140    * @throws IOException
141    */
142   @Test public void testPrepare() throws IOException {
143     prepareGOOD_SPLIT_ROW();
144   }
145 
146   private SplitTransaction prepareGOOD_SPLIT_ROW() {
147     SplitTransaction st = new SplitTransaction(this.parent, GOOD_SPLIT_ROW);
148     assertTrue(st.prepare());
149     return st;
150   }
151 
152   /**
153    * Pass a reference store
154    */
155   @Test public void testPrepareWithRegionsWithReference() throws IOException {
156     // create a mock that will act as a reference StoreFile
157     StoreFile storeFileMock  = Mockito.mock(StoreFile.class);
158     when(storeFileMock.isReference()).thenReturn(true);
159 
160     // add the mock to the parent stores
161     Store storeMock = Mockito.mock(Store.class);
162     List<StoreFile> storeFileList = new ArrayList<StoreFile>(1);
163     storeFileList.add(storeFileMock);
164     when(storeMock.getStorefiles()).thenReturn(storeFileList);
165     when(storeMock.close()).thenReturn(ImmutableList.copyOf(storeFileList));
166     this.parent.stores.put(Bytes.toBytes(""), storeMock);
167 
168     SplitTransaction st = new SplitTransaction(this.parent, GOOD_SPLIT_ROW);
169 
170     assertFalse("a region should not be splittable if it has instances of store file references",
171                 st.prepare());
172   }
173 
174   /**
175    * Pass an unreasonable split row.
176    */
177   @Test public void testPrepareWithBadSplitRow() throws IOException {
178     // Pass start row as split key.
179     SplitTransaction st = new SplitTransaction(this.parent, STARTROW);
180     assertFalse(st.prepare());
181     st = new SplitTransaction(this.parent, HConstants.EMPTY_BYTE_ARRAY);
182     assertFalse(st.prepare());
183     st = new SplitTransaction(this.parent, new byte [] {'A', 'A', 'A'});
184     assertFalse(st.prepare());
185     st = new SplitTransaction(this.parent, ENDROW);
186     assertFalse(st.prepare());
187   }
188 
189   @Test public void testPrepareWithClosedRegion() throws IOException {
190     this.parent.close();
191     SplitTransaction st = new SplitTransaction(this.parent, GOOD_SPLIT_ROW);
192     assertFalse(st.prepare());
193   }
194 
195   @Test public void testWholesomeSplitWithHFileV1() throws IOException {
196     int defaultVersion = TEST_UTIL.getConfiguration().getInt(
197         HFile.FORMAT_VERSION_KEY, 2);
198     TEST_UTIL.getConfiguration().setInt(HFile.FORMAT_VERSION_KEY, 1);
199     try {
200       for (Store store : this.parent.stores.values()) {
201         store.getFamily().setBloomFilterType(StoreFile.BloomType.ROW);
202       }
203       testWholesomeSplit();
204     } finally {
205       TEST_UTIL.getConfiguration().setInt(HFile.FORMAT_VERSION_KEY,
206           defaultVersion);
207     }
208   }
209 
210   @Test public void testWholesomeSplit() throws IOException {
211     final int rowcount = TEST_UTIL.loadRegion(this.parent, CF, true);
212     assertTrue(rowcount > 0);
213     int parentRowCount = countRows(this.parent);
214     assertEquals(rowcount, parentRowCount);
215 
216     // Pretend region's blocks are not in the cache, used for
217     // testWholesomeSplitWithHFileV1
218     CacheConfig cacheConf = new CacheConfig(TEST_UTIL.getConfiguration());
219     ((LruBlockCache) cacheConf.getBlockCache()).clearCache();
220 
221     // Start transaction.
222     SplitTransaction st = prepareGOOD_SPLIT_ROW();
223 
224     // Run the execute.  Look at what it returns.
225     Server mockServer = Mockito.mock(Server.class);
226     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
227     PairOfSameType<HRegion> daughters = st.execute(mockServer, null);
228     // Do some assertions about execution.
229     assertTrue(this.fs.exists(st.getSplitDir()));
230     // Assert the parent region is closed.
231     assertTrue(this.parent.isClosed());
232 
233     // Assert splitdir is empty -- because its content will have been moved out
234     // to be under the daughter region dirs.
235     assertEquals(0, this.fs.listStatus(st.getSplitDir()).length);
236     // Check daughters have correct key span.
237     assertTrue(Bytes.equals(this.parent.getStartKey(),
238       daughters.getFirst().getStartKey()));
239     assertTrue(Bytes.equals(GOOD_SPLIT_ROW,
240       daughters.getFirst().getEndKey()));
241     assertTrue(Bytes.equals(daughters.getSecond().getStartKey(),
242       GOOD_SPLIT_ROW));
243     assertTrue(Bytes.equals(this.parent.getEndKey(),
244       daughters.getSecond().getEndKey()));
245     // Count rows.
246     int daughtersRowCount = 0;
247     for (HRegion r: daughters) {
248       // Open so can count its content.
249       HRegion openRegion = HRegion.openHRegion(this.testdir, r.getRegionInfo(),
250          r.getTableDesc(), r.getLog(), r.getConf());
251       try {
252         int count = countRows(openRegion);
253         assertTrue(count > 0 && count != rowcount);
254         daughtersRowCount += count;
255       } finally {
256         openRegion.close();
257         openRegion.getLog().closeAndDelete();
258       }
259     }
260     assertEquals(rowcount, daughtersRowCount);
261     // Assert the write lock is no longer held on parent
262     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
263   }
264 
265   @Test public void testRollback() throws IOException {
266     final int rowcount = TEST_UTIL.loadRegion(this.parent, CF);
267     assertTrue(rowcount > 0);
268     int parentRowCount = countRows(this.parent);
269     assertEquals(rowcount, parentRowCount);
270 
271     // Start transaction.
272     SplitTransaction st = prepareGOOD_SPLIT_ROW();
273     SplitTransaction spiedUponSt = spy(st);
274     when(spiedUponSt.createDaughterRegion(spiedUponSt.getSecondDaughter(), null)).
275       thenThrow(new MockedFailedDaughterCreation());
276     // Run the execute.  Look at what it returns.
277     boolean expectedException = false;
278     Server mockServer = Mockito.mock(Server.class);
279     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
280     try {
281       spiedUponSt.execute(mockServer, null);
282     } catch (MockedFailedDaughterCreation e) {
283       expectedException = true;
284     }
285     assertTrue(expectedException);
286     // Run rollback
287     assertTrue(spiedUponSt.rollback(null, null));
288 
289     // Assert I can scan parent.
290     int parentRowCount2 = countRows(this.parent);
291     assertEquals(parentRowCount, parentRowCount2);
292 
293     // Assert rollback cleaned up stuff in fs
294     assertTrue(!this.fs.exists(HRegion.getRegionDir(this.testdir, st.getFirstDaughter())));
295     assertTrue(!this.fs.exists(HRegion.getRegionDir(this.testdir, st.getSecondDaughter())));
296     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
297 
298     // Now retry the split but do not throw an exception this time.
299     assertTrue(st.prepare());
300     PairOfSameType<HRegion> daughters = st.execute(mockServer, null);
301     // Count rows.
302     int daughtersRowCount = 0;
303     for (HRegion r: daughters) {
304       // Open so can count its content.
305       HRegion openRegion = HRegion.openHRegion(this.testdir, r.getRegionInfo(),
306          r.getTableDesc(), r.getLog(), r.getConf());
307       try {
308         int count = countRows(openRegion);
309         assertTrue(count > 0 && count != rowcount);
310         daughtersRowCount += count;
311       } finally {
312         openRegion.close();
313         openRegion.getLog().closeAndDelete();
314       }
315     }
316     assertEquals(rowcount, daughtersRowCount);
317     // Assert the write lock is no longer held on parent
318     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
319   }
320 
321   /**
322    * Exception used in this class only.
323    */
324   @SuppressWarnings("serial")
325   private class MockedFailedDaughterCreation extends IOException {}
326   private class MockedFailedDaughterOpen extends IOException {}
327 
328   private int countRows(final HRegion r) throws IOException {
329     int rowcount = 0;
330     InternalScanner scanner = r.getScanner(new Scan());
331     try {
332       List<KeyValue> kvs = new ArrayList<KeyValue>();
333       boolean hasNext = true;
334       while (hasNext) {
335         hasNext = scanner.next(kvs);
336         if (!kvs.isEmpty()) rowcount++;
337       }
338     } finally {
339       scanner.close();
340     }
341     return rowcount;
342   }
343 
344   HRegion createRegion(final Path testdir, final HLog wal)
345   throws IOException {
346     // Make a region with start and end keys. Use 'aaa', to 'AAA'.  The load
347     // region utility will add rows between 'aaa' and 'zzz'.
348     HTableDescriptor htd = new HTableDescriptor("table");
349     HColumnDescriptor hcd = new HColumnDescriptor(CF);
350     htd.addFamily(hcd);
351     HRegionInfo hri = new HRegionInfo(htd.getName(), STARTROW, ENDROW);
352     HRegion r = HRegion.createHRegion(hri, testdir, TEST_UTIL.getConfiguration(), htd);
353     r.close();
354     r.getLog().closeAndDelete();
355     return HRegion.openHRegion(testdir, hri, htd, wal,
356       TEST_UTIL.getConfiguration());
357   }
358 
359   @org.junit.Rule
360   public org.apache.hadoop.hbase.ResourceCheckerJUnitRule cu =
361     new org.apache.hadoop.hbase.ResourceCheckerJUnitRule();
362 }
363