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