View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  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,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations 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.Matchers.anyInt;
25  import static org.mockito.Matchers.eq;
26  import static org.mockito.Mockito.*;
27  
28  import org.mockito.Mockito;
29  
30  import java.io.IOException;
31  import java.util.ArrayList;
32  import java.util.List;
33  
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.fs.FileSystem;
36  import org.apache.hadoop.fs.Path;
37  import org.apache.hadoop.hbase.Cell;
38  import org.apache.hadoop.hbase.HBaseTestingUtility;
39  import org.apache.hadoop.hbase.HColumnDescriptor;
40  import org.apache.hadoop.hbase.HConstants;
41  import org.apache.hadoop.hbase.HRegionInfo;
42  import org.apache.hadoop.hbase.HTableDescriptor;
43  import org.apache.hadoop.hbase.Server;
44  import org.apache.hadoop.hbase.TableName;
45  import org.apache.hadoop.hbase.client.Scan;
46  import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver;
47  import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
48  import org.apache.hadoop.hbase.coprocessor.ObserverContext;
49  import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
50  import org.apache.hadoop.hbase.io.hfile.CacheConfig;
51  import org.apache.hadoop.hbase.io.hfile.LruBlockCache;
52  import org.apache.hadoop.hbase.testclassification.SmallTests;
53  import org.apache.hadoop.hbase.util.Bytes;
54  import org.apache.hadoop.hbase.util.FSUtils;
55  import org.apache.hadoop.hbase.util.PairOfSameType;
56  import org.apache.hadoop.hbase.wal.WALFactory;
57  import org.apache.zookeeper.KeeperException;
58  import org.junit.After;
59  import org.junit.Before;
60  import org.junit.Test;
61  import org.junit.experimental.categories.Category;
62  
63  import com.google.common.collect.ImmutableList;
64  
65  /**
66   * Test the {@link SplitTransactionImpl} class against an HRegion (as opposed to
67   * running cluster).
68   */
69  @Category(SmallTests.class)
70  public class TestSplitTransaction {
71    private final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
72    private final Path testdir =
73      TEST_UTIL.getDataTestDir(this.getClass().getName());
74    private HRegion parent;
75    private WALFactory wals;
76    private FileSystem fs;
77    private static final byte [] STARTROW = new byte [] {'a', 'a', 'a'};
78    // '{' is next ascii after 'z'.
79    private static final byte [] ENDROW = new byte [] {'{', '{', '{'};
80    private static final byte [] GOOD_SPLIT_ROW = new byte [] {'d', 'd', 'd'};
81    private static final byte [] CF = HConstants.CATALOG_FAMILY;
82    
83    private static boolean preRollBackCalled = false;
84    private static boolean postRollBackCalled = false;
85    
86    @Before
87    public void setup() throws IOException {
88      this.fs = FileSystem.get(TEST_UTIL.getConfiguration());
89      TEST_UTIL.getConfiguration().set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, CustomObserver.class.getName());
90      this.fs.delete(this.testdir, true);
91      final Configuration walConf = new Configuration(TEST_UTIL.getConfiguration());
92      FSUtils.setRootDir(walConf, this.testdir);
93      this.wals = new WALFactory(walConf, null, this.getClass().getName());
94      
95      this.parent = createRegion(this.testdir, this.wals);
96      RegionCoprocessorHost host = new RegionCoprocessorHost(this.parent, null, TEST_UTIL.getConfiguration());
97      this.parent.setCoprocessorHost(host);
98      TEST_UTIL.getConfiguration().setBoolean("hbase.testing.nocluster", true);
99    }
100 
101   @After
102   public void teardown() throws IOException {
103     if (this.parent != null && !this.parent.isClosed()) this.parent.close();
104     if (this.parent != null) {
105       Path regionDir = this.parent.getRegionFileSystem().getRegionDir();
106       if (this.fs.exists(regionDir) && !this.fs.delete(regionDir, true)) {
107         throw new IOException("Failed delete of " + regionDir);
108       }
109     }
110     if (this.wals != null) {
111       this.wals.close();
112     }
113     this.fs.delete(this.testdir, true);
114   }
115 
116   @Test
117   public void testFailAfterPONR() throws IOException, KeeperException {
118     final int rowcount = TEST_UTIL.loadRegion(this.parent, CF);
119     assertTrue(rowcount > 0);
120     int parentRowCount = countRows(this.parent);
121     assertEquals(rowcount, parentRowCount);
122 
123     // Start transaction.
124     SplitTransactionImpl st = prepareGOOD_SPLIT_ROW();
125     SplitTransactionImpl spiedUponSt = spy(st);
126     Mockito
127         .doThrow(new MockedFailedDaughterOpen())
128         .when(spiedUponSt)
129         .openDaughterRegion((Server) Mockito.anyObject(),
130             (HRegion) Mockito.anyObject());
131 
132     // Run the execute.  Look at what it returns.
133     boolean expectedException = false;
134     Server mockServer = Mockito.mock(Server.class);
135     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
136     try {
137       spiedUponSt.execute(mockServer, null);
138     } catch (IOException e) {
139       if (e.getCause() != null &&
140           e.getCause() instanceof MockedFailedDaughterOpen) {
141         expectedException = true;
142       }
143     }
144     assertTrue(expectedException);
145     // Run rollback returns that we should restart.
146     assertFalse(spiedUponSt.rollback(null, null));
147     // Make sure that region a and region b are still in the filesystem, that
148     // they have not been removed; this is supposed to be the case if we go
149     // past point of no return.
150     Path tableDir =  this.parent.getRegionFileSystem().getTableDir();
151     Path daughterADir = new Path(tableDir, spiedUponSt.getFirstDaughter().getEncodedName());
152     Path daughterBDir = new Path(tableDir, spiedUponSt.getSecondDaughter().getEncodedName());
153     assertTrue(TEST_UTIL.getTestFileSystem().exists(daughterADir));
154     assertTrue(TEST_UTIL.getTestFileSystem().exists(daughterBDir));
155   }
156 
157   /**
158    * Test straight prepare works.  Tries to split on {@link #GOOD_SPLIT_ROW}
159    * @throws IOException
160    */
161   @Test
162   public void testPrepare() throws IOException {
163     prepareGOOD_SPLIT_ROW();
164   }
165 
166   private SplitTransactionImpl prepareGOOD_SPLIT_ROW() throws IOException {
167     return prepareGOOD_SPLIT_ROW(this.parent);
168   }
169 
170   private SplitTransactionImpl prepareGOOD_SPLIT_ROW(final HRegion parentRegion)
171       throws IOException {
172     SplitTransactionImpl st = new SplitTransactionImpl(parentRegion, GOOD_SPLIT_ROW);
173     assertTrue(st.prepare());
174     return st;
175   }
176 
177   /**
178    * Pass a reference store
179    */
180   @Test
181   public void testPrepareWithRegionsWithReference() throws IOException {
182     HStore storeMock = Mockito.mock(HStore.class);
183     when(storeMock.hasReferences()).thenReturn(true);
184     when(storeMock.getFamily()).thenReturn(new HColumnDescriptor("cf"));
185     when(storeMock.close()).thenReturn(ImmutableList.<StoreFile>of());
186     this.parent.stores.put(Bytes.toBytes(""), storeMock);
187 
188     SplitTransactionImpl st = new SplitTransactionImpl(this.parent, GOOD_SPLIT_ROW);
189 
190     assertFalse("a region should not be splittable if it has instances of store file references",
191                 st.prepare());
192   }
193 
194   /**
195    * Test SplitTransactionListener
196    */
197   @Test
198   public void testSplitTransactionListener() throws IOException {
199     SplitTransactionImpl st = new SplitTransactionImpl(this.parent, GOOD_SPLIT_ROW);
200     SplitTransaction.TransactionListener listener =
201             Mockito.mock(SplitTransaction.TransactionListener.class);
202     st.registerTransactionListener(listener);
203     st.prepare();
204     Server mockServer = Mockito.mock(Server.class);
205     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
206     PairOfSameType<Region> daughters = st.execute(mockServer, null);
207     verify(listener).transition(st, SplitTransaction.SplitTransactionPhase.STARTED,
208             SplitTransaction.SplitTransactionPhase.PREPARED);
209     verify(listener, times(15)).transition(any(SplitTransaction.class),
210             any(SplitTransaction.SplitTransactionPhase.class),
211             any(SplitTransaction.SplitTransactionPhase.class));
212     verifyNoMoreInteractions(listener);
213   }
214 
215   /**
216    * Pass an unreasonable split row.
217    */
218   @Test
219   public void testPrepareWithBadSplitRow() throws IOException {
220     // Pass start row as split key.
221     SplitTransactionImpl st = new SplitTransactionImpl(this.parent, STARTROW);
222     assertFalse(st.prepare());
223     st = new SplitTransactionImpl(this.parent, HConstants.EMPTY_BYTE_ARRAY);
224     assertFalse(st.prepare());
225     st = new SplitTransactionImpl(this.parent, new byte [] {'A', 'A', 'A'});
226     assertFalse(st.prepare());
227     st = new SplitTransactionImpl(this.parent, ENDROW);
228     assertFalse(st.prepare());
229   }
230 
231   @Test
232   public void testPrepareWithClosedRegion() throws IOException {
233     this.parent.close();
234     SplitTransactionImpl st = new SplitTransactionImpl(this.parent, GOOD_SPLIT_ROW);
235     assertFalse(st.prepare());
236   }
237 
238   @Test
239   public void testWholesomeSplit() throws IOException {
240     final int rowcount = TEST_UTIL.loadRegion(this.parent, CF, true);
241     assertTrue(rowcount > 0);
242     int parentRowCount = countRows(this.parent);
243     assertEquals(rowcount, parentRowCount);
244 
245     // Pretend region's blocks are not in the cache, used for
246     // testWholesomeSplitWithHFileV1
247     CacheConfig cacheConf = new CacheConfig(TEST_UTIL.getConfiguration());
248     ((LruBlockCache) cacheConf.getBlockCache()).clearCache();
249 
250     // Start transaction.
251     SplitTransactionImpl st = prepareGOOD_SPLIT_ROW();
252 
253     // Run the execute.  Look at what it returns.
254     Server mockServer = Mockito.mock(Server.class);
255     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
256     PairOfSameType<Region> daughters = st.execute(mockServer, null);
257     // Do some assertions about execution.
258     assertTrue(this.fs.exists(this.parent.getRegionFileSystem().getSplitsDir()));
259     // Assert the parent region is closed.
260     assertTrue(this.parent.isClosed());
261 
262     // Assert splitdir is empty -- because its content will have been moved out
263     // to be under the daughter region dirs.
264     assertEquals(0, this.fs.listStatus(this.parent.getRegionFileSystem().getSplitsDir()).length);
265     // Check daughters have correct key span.
266     assertTrue(Bytes.equals(parent.getRegionInfo().getStartKey(),
267       daughters.getFirst().getRegionInfo().getStartKey()));
268     assertTrue(Bytes.equals(GOOD_SPLIT_ROW, daughters.getFirst().getRegionInfo().getEndKey()));
269     assertTrue(Bytes.equals(daughters.getSecond().getRegionInfo().getStartKey(), GOOD_SPLIT_ROW));
270     assertTrue(Bytes.equals(parent.getRegionInfo().getEndKey(),
271       daughters.getSecond().getRegionInfo().getEndKey()));
272     // Count rows. daughters are already open
273     int daughtersRowCount = 0;
274     for (Region openRegion: daughters) {
275       try {
276         int count = countRows(openRegion);
277         assertTrue(count > 0 && count != rowcount);
278         daughtersRowCount += count;
279       } finally {
280         HRegion.closeHRegion((HRegion)openRegion);
281       }
282     }
283     assertEquals(rowcount, daughtersRowCount);
284     // Assert the write lock is no longer held on parent
285     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
286   }
287 
288   @Test
289   public void testCountReferencesFailsSplit() throws IOException {
290     final int rowcount = TEST_UTIL.loadRegion(this.parent, CF);
291     assertTrue(rowcount > 0);
292     int parentRowCount = countRows(this.parent);
293     assertEquals(rowcount, parentRowCount);
294 
295     // Start transaction.
296     HRegion spiedRegion = spy(this.parent);
297     SplitTransactionImpl st = prepareGOOD_SPLIT_ROW(spiedRegion);
298     SplitTransactionImpl spiedUponSt = spy(st);
299     doThrow(new IOException("Failing split. Expected reference file count isn't equal."))
300         .when(spiedUponSt).assertReferenceFileCount(anyInt(),
301         eq(new Path(this.parent.getRegionFileSystem().getTableDir(),
302             st.getSecondDaughter().getEncodedName())));
303 
304     // Run the execute.  Look at what it returns.
305     boolean expectedException = false;
306     Server mockServer = Mockito.mock(Server.class);
307     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
308     try {
309       spiedUponSt.execute(mockServer, null);
310     } catch (IOException e) {
311       expectedException = true;
312     }
313     assertTrue(expectedException);
314   }
315 
316   @Test
317   public void testRollback() throws IOException {
318     final int rowcount = TEST_UTIL.loadRegion(this.parent, CF);
319     assertTrue(rowcount > 0);
320     int parentRowCount = countRows(this.parent);
321     assertEquals(rowcount, parentRowCount);
322 
323     // Start transaction.
324     HRegion spiedRegion = spy(this.parent);
325     SplitTransactionImpl st = prepareGOOD_SPLIT_ROW(spiedRegion);
326     SplitTransactionImpl spiedUponSt = spy(st);
327     doNothing().when(spiedUponSt).assertReferenceFileCount(anyInt(),
328         eq(parent.getRegionFileSystem().getSplitsDir(st.getFirstDaughter())));
329     when(spiedRegion.createDaughterRegionFromSplits(spiedUponSt.getSecondDaughter())).
330         thenThrow(new MockedFailedDaughterCreation());
331     // Run the execute.  Look at what it returns.
332     boolean expectedException = false;
333     Server mockServer = Mockito.mock(Server.class);
334     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
335     try {
336       spiedUponSt.execute(mockServer, null);
337     } catch (MockedFailedDaughterCreation e) {
338       expectedException = true;
339     }
340     assertTrue(expectedException);
341     // Run rollback
342     assertTrue(spiedUponSt.rollback(null, null));
343 
344     // Assert I can scan parent.
345     int parentRowCount2 = countRows(this.parent);
346     assertEquals(parentRowCount, parentRowCount2);
347 
348     // Assert rollback cleaned up stuff in fs
349     assertTrue(!this.fs.exists(HRegion.getRegionDir(this.testdir, st.getFirstDaughter())));
350     assertTrue(!this.fs.exists(HRegion.getRegionDir(this.testdir, st.getSecondDaughter())));
351     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
352 
353     // Now retry the split but do not throw an exception this time.
354     assertTrue(st.prepare());
355     PairOfSameType<Region> daughters = st.execute(mockServer, null);
356     // Count rows. daughters are already open
357     int daughtersRowCount = 0;
358     for (Region openRegion: daughters) {
359       try {
360         int count = countRows(openRegion);
361         assertTrue(count > 0 && count != rowcount);
362         daughtersRowCount += count;
363       } finally {
364         HRegion.closeHRegion((HRegion)openRegion);
365       }
366     }
367     assertEquals(rowcount, daughtersRowCount);
368     // Assert the write lock is no longer held on parent
369     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
370     assertTrue("Rollback hooks should be called.", wasRollBackHookCalled());
371   }
372   
373   private boolean wasRollBackHookCalled(){
374     return (preRollBackCalled && postRollBackCalled);
375   }
376 
377   /**
378    * Exception used in this class only.
379    */
380   @SuppressWarnings("serial")
381   private class MockedFailedDaughterCreation extends IOException {}
382   private class MockedFailedDaughterOpen extends IOException {}
383 
384   private int countRows(final Region r) throws IOException {
385     int rowcount = 0;
386     InternalScanner scanner = r.getScanner(new Scan());
387     try {
388       List<Cell> kvs = new ArrayList<Cell>();
389       boolean hasNext = true;
390       while (hasNext) {
391         hasNext = scanner.next(kvs);
392         if (!kvs.isEmpty()) rowcount++;
393       }
394     } finally {
395       scanner.close();
396     }
397     return rowcount;
398   }
399 
400   HRegion createRegion(final Path testdir, final WALFactory wals)
401   throws IOException {
402     // Make a region with start and end keys. Use 'aaa', to 'AAA'.  The load
403     // region utility will add rows between 'aaa' and 'zzz'.
404     HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("table"));
405     HColumnDescriptor hcd = new HColumnDescriptor(CF);
406     htd.addFamily(hcd);
407     HRegionInfo hri = new HRegionInfo(htd.getTableName(), STARTROW, ENDROW);
408     HRegion r = HRegion.createHRegion(hri, testdir, TEST_UTIL.getConfiguration(), htd);
409     HRegion.closeHRegion(r);
410     return HRegion.openHRegion(testdir, hri, htd,
411       wals.getWAL(hri.getEncodedNameAsBytes(), hri.getTable().getNamespace()),
412       TEST_UTIL.getConfiguration());
413   }
414   
415   public static class CustomObserver extends BaseRegionObserver{
416     @Override
417     public void preRollBackSplit(
418         ObserverContext<RegionCoprocessorEnvironment> ctx) throws IOException {
419       preRollBackCalled = true;
420     }
421     
422     @Override
423     public void postRollBackSplit(
424         ObserverContext<RegionCoprocessorEnvironment> ctx) throws IOException {
425       postRollBackCalled = true;
426     }
427   }
428 
429 }