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.client.Scan;
42  import org.apache.hadoop.hbase.regionserver.wal.HLog;
43  import org.apache.hadoop.hbase.util.Bytes;
44  import org.apache.hadoop.hbase.util.PairOfSameType;
45  import org.apache.zookeeper.KeeperException;
46  import org.junit.After;
47  import org.junit.Before;
48  import org.junit.Test;
49  import org.mockito.Mockito;
50  
51  /**
52   * Test the {@link SplitTransaction} class against an HRegion (as opposed to
53   * running cluster).
54   */
55  public class TestSplitTransaction {
56    private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
57    private final Path testdir =
58      HBaseTestingUtility.getTestDir(this.getClass().getName());
59    private HRegion parent;
60    private HLog wal;
61    private FileSystem fs;
62    private static final byte [] STARTROW = new byte [] {'a', 'a', 'a'};
63    // '{' is next ascii after 'z'.
64    private static final byte [] ENDROW = new byte [] {'{', '{', '{'};
65    private static final byte [] GOOD_SPLIT_ROW = new byte [] {'d', 'd', 'd'};
66    private static final byte [] CF = HConstants.CATALOG_FAMILY;
67  
68    @Before public void setup() throws IOException {
69      this.fs = FileSystem.get(TEST_UTIL.getConfiguration());
70      this.fs.delete(this.testdir, true);
71      this.wal = new HLog(fs, new Path(this.testdir, "logs"),
72        new Path(this.testdir, "archive"),
73        TEST_UTIL.getConfiguration());
74      this.parent = createRegion(this.testdir, this.wal);
75      TEST_UTIL.getConfiguration().setBoolean("hbase.testing.nocluster", true);
76    }
77  
78    @After public void teardown() throws IOException {
79      if (this.parent != null && !this.parent.isClosed()) this.parent.close();
80      if (this.fs.exists(this.parent.getRegionDir()) &&
81          !this.fs.delete(this.parent.getRegionDir(), true)) {
82        throw new IOException("Failed delete of " + this.parent.getRegionDir());
83      }
84      if (this.wal != null) this.wal.closeAndDelete();
85      this.fs.delete(this.testdir, true);
86    }
87  
88    @Test public void testFailAfterPONR() throws IOException, KeeperException {
89      final int rowcount = TEST_UTIL.loadRegion(this.parent, CF);
90      assertTrue(rowcount > 0);
91      int parentRowCount = countRows(this.parent);
92      assertEquals(rowcount, parentRowCount);
93  
94      // Start transaction.
95      SplitTransaction st = prepareGOOD_SPLIT_ROW();
96      SplitTransaction spiedUponSt = spy(st);
97      Mockito.doThrow(new MockedFailedDaughterOpen()).
98        when(spiedUponSt).openDaughterRegion((Server)Mockito.anyObject(),
99          (RegionServerServices)Mockito.anyObject(), (HRegion)Mockito.anyObject());
100 
101     // Run the execute.  Look at what it returns.
102     boolean expectedException = false;
103     Server mockServer = Mockito.mock(Server.class);
104     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
105     try {
106       spiedUponSt.execute(mockServer, null);
107     } catch (IOException e) {
108       if (e.getCause() != null &&
109           e.getCause() instanceof MockedFailedDaughterOpen) {
110         expectedException = true;
111       }
112     }
113     assertTrue(expectedException);
114     // Run rollback returns that we should restart.
115     assertFalse(spiedUponSt.rollback(null, null));
116     // Make sure that region a and region b are still in the filesystem, that
117     // they have not been removed; this is supposed to be the case if we go
118     // past point of no return.
119     Path tableDir =  this.parent.getRegionDir().getParent();
120     Path daughterADir =
121       new Path(tableDir, spiedUponSt.getFirstDaughter().getEncodedName());
122     Path daughterBDir =
123       new Path(tableDir, spiedUponSt.getSecondDaughter().getEncodedName());
124     assertTrue(TEST_UTIL.getTestFileSystem().exists(daughterADir));
125     assertTrue(TEST_UTIL.getTestFileSystem().exists(daughterBDir));
126   }
127 
128   /**
129    * Test straight prepare works.  Tries to split on {@link #GOOD_SPLIT_ROW}
130    * @throws IOException
131    */
132   @Test public void testPrepare() throws IOException {
133     prepareGOOD_SPLIT_ROW();
134   }
135 
136   private SplitTransaction prepareGOOD_SPLIT_ROW() {
137     SplitTransaction st = new SplitTransaction(this.parent, GOOD_SPLIT_ROW);
138     assertTrue(st.prepare());
139     return st;
140   }
141 
142   /**
143    * Pass an unreasonable split row.
144    */
145   @Test public void testPrepareWithBadSplitRow() throws IOException {
146     // Pass start row as split key.
147     SplitTransaction st = new SplitTransaction(this.parent, STARTROW);
148     assertFalse(st.prepare());
149     st = new SplitTransaction(this.parent, HConstants.EMPTY_BYTE_ARRAY);
150     assertFalse(st.prepare());
151     st = new SplitTransaction(this.parent, new byte [] {'A', 'A', 'A'});
152     assertFalse(st.prepare());
153     st = new SplitTransaction(this.parent, ENDROW);
154     assertFalse(st.prepare());
155   }
156 
157   @Test public void testPrepareWithClosedRegion() throws IOException {
158     this.parent.close();
159     SplitTransaction st = new SplitTransaction(this.parent, GOOD_SPLIT_ROW);
160     assertFalse(st.prepare());
161   }
162 
163   @Test public void testWholesomeSplit() throws IOException {
164     final int rowcount = TEST_UTIL.loadRegion(this.parent, CF);
165     assertTrue(rowcount > 0);
166     int parentRowCount = countRows(this.parent);
167     assertEquals(rowcount, parentRowCount);
168 
169     // Start transaction.
170     SplitTransaction st = prepareGOOD_SPLIT_ROW();
171 
172     // Run the execute.  Look at what it returns.
173     Server mockServer = Mockito.mock(Server.class);
174     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
175     PairOfSameType<HRegion> daughters = st.execute(mockServer, null);
176     // Do some assertions about execution.
177     assertTrue(this.fs.exists(st.getSplitDir()));
178     // Assert the parent region is closed.
179     assertTrue(this.parent.isClosed());
180 
181     // Assert splitdir is empty -- because its content will have been moved out
182     // to be under the daughter region dirs.
183     assertEquals(0, this.fs.listStatus(st.getSplitDir()).length);
184     // Check daughters have correct key span.
185     assertTrue(Bytes.equals(this.parent.getStartKey(),
186       daughters.getFirst().getStartKey()));
187     assertTrue(Bytes.equals(GOOD_SPLIT_ROW,
188       daughters.getFirst().getEndKey()));
189     assertTrue(Bytes.equals(daughters.getSecond().getStartKey(),
190       GOOD_SPLIT_ROW));
191     assertTrue(Bytes.equals(this.parent.getEndKey(),
192       daughters.getSecond().getEndKey()));
193     // Count rows.
194     int daughtersRowCount = 0;
195     for (HRegion r: daughters) {
196       // Open so can count its content.
197       HRegion openRegion = HRegion.openHRegion(r.getRegionInfo(),
198         r.getLog(), r.getConf());
199       try {
200         int count = countRows(openRegion);
201         assertTrue(count > 0 && count != rowcount);
202         daughtersRowCount += count;
203       } finally {
204         openRegion.close();
205       }
206     }
207     assertEquals(rowcount, daughtersRowCount);
208     // Assert the write lock is no longer held on parent
209     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
210   }
211 
212   @Test public void testRollback() throws IOException {
213     final int rowcount = TEST_UTIL.loadRegion(this.parent, CF);
214     assertTrue(rowcount > 0);
215     int parentRowCount = countRows(this.parent);
216     assertEquals(rowcount, parentRowCount);
217 
218     // Start transaction.
219     SplitTransaction st = prepareGOOD_SPLIT_ROW();
220     SplitTransaction spiedUponSt = spy(st);
221     when(spiedUponSt.createDaughterRegion(spiedUponSt.getSecondDaughter(), null)).
222       thenThrow(new MockedFailedDaughterCreation());
223     // Run the execute.  Look at what it returns.
224     boolean expectedException = false;
225     Server mockServer = Mockito.mock(Server.class);
226     when(mockServer.getConfiguration()).thenReturn(TEST_UTIL.getConfiguration());
227     try {
228       spiedUponSt.execute(mockServer, null);
229     } catch (MockedFailedDaughterCreation e) {
230       expectedException = true;
231     }
232     assertTrue(expectedException);
233     // Run rollback
234     assertTrue(spiedUponSt.rollback(null, null));
235 
236     // Assert I can scan parent.
237     int parentRowCount2 = countRows(this.parent);
238     assertEquals(parentRowCount, parentRowCount2);
239 
240     // Assert rollback cleaned up stuff in fs
241     assertTrue(!this.fs.exists(HRegion.getRegionDir(this.testdir, st.getFirstDaughter())));
242     assertTrue(!this.fs.exists(HRegion.getRegionDir(this.testdir, st.getSecondDaughter())));
243     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
244 
245     // Now retry the split but do not throw an exception this time.
246     assertTrue(st.prepare());
247     PairOfSameType<HRegion> daughters = st.execute(mockServer, null);
248     // Count rows.
249     int daughtersRowCount = 0;
250     for (HRegion r: daughters) {
251       // Open so can count its content.
252       HRegion openRegion = HRegion.openHRegion(r.getRegionInfo(),
253         r.getLog(), r.getConf());
254       try {
255         int count = countRows(openRegion);
256         assertTrue(count > 0 && count != rowcount);
257         daughtersRowCount += count;
258       } finally {
259         openRegion.close();
260       }
261     }
262     assertEquals(rowcount, daughtersRowCount);
263     // Assert the write lock is no longer held on parent
264     assertTrue(!this.parent.lock.writeLock().isHeldByCurrentThread());
265   }
266 
267   /**
268    * Exception used in this class only.
269    */
270   @SuppressWarnings("serial")
271   private class MockedFailedDaughterCreation extends IOException {}
272   private class MockedFailedDaughterOpen extends IOException {}
273 
274   private int countRows(final HRegion r) throws IOException {
275     int rowcount = 0;
276     InternalScanner scanner = r.getScanner(new Scan());
277     try {
278       List<KeyValue> kvs = new ArrayList<KeyValue>();
279       boolean hasNext = true;
280       while (hasNext) {
281         hasNext = scanner.next(kvs);
282         if (!kvs.isEmpty()) rowcount++;
283       }
284     } finally {
285       scanner.close();
286     }
287     return rowcount;
288   }
289 
290   static HRegion createRegion(final Path testdir, final HLog wal)
291   throws IOException {
292     // Make a region with start and end keys. Use 'aaa', to 'AAA'.  The load
293     // region utility will add rows between 'aaa' and 'zzz'.
294     HTableDescriptor htd = new HTableDescriptor("table");
295     HColumnDescriptor hcd = new HColumnDescriptor(CF);
296     htd.addFamily(hcd);
297     HRegionInfo hri = new HRegionInfo(htd, STARTROW, ENDROW);
298     return HRegion.openHRegion(hri, wal, TEST_UTIL.getConfiguration());
299   }
300 }