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.mapreduce;
20  
21  import static org.junit.Assert.assertTrue;
22  import static org.junit.Assert.fail;
23  
24  import java.io.IOException;
25  import java.util.Iterator;
26  import java.util.Map;
27  import java.util.NavigableMap;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.hadoop.conf.Configuration;
31  import org.apache.hadoop.hbase.CategoryBasedTimeout;
32  import org.apache.hadoop.hbase.Cell;
33  import org.apache.hadoop.hbase.CellUtil;
34  import org.apache.hadoop.hbase.HBaseTestingUtility;
35  import org.apache.hadoop.hbase.HConstants;
36  import org.apache.hadoop.hbase.TableName;
37  import org.apache.hadoop.hbase.client.HTable;
38  import org.apache.hadoop.hbase.client.Put;
39  import org.apache.hadoop.hbase.client.Result;
40  import org.apache.hadoop.hbase.client.ResultScanner;
41  import org.apache.hadoop.hbase.client.Scan;
42  import org.apache.hadoop.hbase.client.Table;
43  import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
44  import org.apache.hadoop.hbase.util.Bytes;
45  import org.junit.AfterClass;
46  import org.junit.BeforeClass;
47  import org.junit.Rule;
48  import org.junit.Test;
49  import org.junit.rules.TestRule;
50  
51  /**
52   * A base class for a test Map/Reduce job over HBase tables. The map/reduce process we're testing
53   * on our tables is simple - take every row in the table, reverse the value of a particular cell,
54   * and write it back to the table. Implements common components between mapred and mapreduce
55   * implementations.
56   */
57  public abstract class TestTableMapReduceBase {
58    @Rule public final TestRule timeout = CategoryBasedTimeout.builder().
59        withTimeout(this.getClass()).withLookingForStuckThread(true).build();
60    protected static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
61    protected static final TableName MULTI_REGION_TABLE_NAME = TableName.valueOf("mrtest");
62    protected static final TableName TABLE_FOR_NEGATIVE_TESTS = TableName.valueOf("testfailuretable");
63    protected static final byte[] INPUT_FAMILY = Bytes.toBytes("contents");
64    protected static final byte[] OUTPUT_FAMILY = Bytes.toBytes("text");
65  
66    protected static final byte[][] columns = new byte[][] {
67      INPUT_FAMILY,
68      OUTPUT_FAMILY
69    };
70  
71    /**
72     * Retrieve my logger instance.
73     */
74    protected abstract Log getLog();
75  
76    /**
77     * Handles API-specifics for setting up and executing the job.
78     */
79    protected abstract void runTestOnTable(HTable table) throws IOException;
80  
81    @BeforeClass
82    public static void beforeClass() throws Exception {
83      UTIL.setJobWithoutMRCluster();
84      UTIL.startMiniCluster();
85      HTable table =
86          UTIL.createMultiRegionTable(MULTI_REGION_TABLE_NAME, new byte[][] { INPUT_FAMILY,
87              OUTPUT_FAMILY });
88      UTIL.loadTable(table, INPUT_FAMILY, false);
89      UTIL.createTable(TABLE_FOR_NEGATIVE_TESTS, new byte[][] { INPUT_FAMILY, OUTPUT_FAMILY });
90    }
91  
92    @AfterClass
93    public static void afterClass() throws Exception {
94      UTIL.deleteTable(TABLE_FOR_NEGATIVE_TESTS);
95      UTIL.shutdownMiniCluster();
96    }
97  
98    /**
99     * Test a map/reduce against a multi-region table
100    * @throws IOException
101    */
102   @Test
103   public void testMultiRegionTable() throws IOException {
104     runTestOnTable(new HTable(UTIL.getConfiguration(), MULTI_REGION_TABLE_NAME));
105   }
106 
107   @Test
108   public void testCombiner() throws IOException {
109     Configuration conf = new Configuration(UTIL.getConfiguration());
110     // force use of combiner for testing purposes
111     conf.setInt("mapreduce.map.combine.minspills", 1);
112     runTestOnTable(new HTable(conf, MULTI_REGION_TABLE_NAME));
113   }
114 
115   /**
116    * Implements mapper logic for use across APIs.
117    */
118   protected static Put map(ImmutableBytesWritable key, Result value) throws IOException {
119     if (value.size() != 1) {
120       throw new IOException("There should only be one input column");
121     }
122     Map<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>>
123       cf = value.getMap();
124     if(!cf.containsKey(INPUT_FAMILY)) {
125       throw new IOException("Wrong input columns. Missing: '" +
126         Bytes.toString(INPUT_FAMILY) + "'.");
127     }
128 
129     // Get the original value and reverse it
130 
131     String originalValue = Bytes.toString(value.getValue(INPUT_FAMILY, INPUT_FAMILY));
132     StringBuilder newValue = new StringBuilder(originalValue);
133     newValue.reverse();
134 
135     // Now set the value to be collected
136 
137     Put outval = new Put(key.get());
138     outval.add(OUTPUT_FAMILY, null, Bytes.toBytes(newValue.toString()));
139     return outval;
140   }
141 
142   protected void verify(TableName tableName) throws IOException {
143     Table table = new HTable(UTIL.getConfiguration(), tableName);
144     boolean verified = false;
145     long pause = UTIL.getConfiguration().getLong("hbase.client.pause", 5 * 1000);
146     int numRetries = UTIL.getConfiguration().getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 5);
147     for (int i = 0; i < numRetries; i++) {
148       try {
149         getLog().info("Verification attempt #" + i);
150         verifyAttempt(table);
151         verified = true;
152         break;
153       } catch (NullPointerException e) {
154         // If here, a cell was empty. Presume its because updates came in
155         // after the scanner had been opened. Wait a while and retry.
156         getLog().debug("Verification attempt failed: " + e.getMessage());
157       }
158       try {
159         Thread.sleep(pause);
160       } catch (InterruptedException e) {
161         // continue
162       }
163     }
164     assertTrue(verified);
165   }
166 
167   /**
168    * Looks at every value of the mapreduce output and verifies that indeed
169    * the values have been reversed.
170    * @param table Table to scan.
171    * @throws IOException
172    * @throws NullPointerException if we failed to find a cell value
173    */
174   private void verifyAttempt(final Table table) throws IOException, NullPointerException {
175     Scan scan = new Scan();
176     TableInputFormat.addColumns(scan, columns);
177     ResultScanner scanner = table.getScanner(scan);
178     try {
179       Iterator<Result> itr = scanner.iterator();
180       assertTrue(itr.hasNext());
181       while(itr.hasNext()) {
182         Result r = itr.next();
183         if (getLog().isDebugEnabled()) {
184           if (r.size() > 2 ) {
185             throw new IOException("Too many results, expected 2 got " +
186               r.size());
187           }
188         }
189         byte[] firstValue = null;
190         byte[] secondValue = null;
191         int count = 0;
192          for(Cell kv : r.listCells()) {
193           if (count == 0) {
194             firstValue = CellUtil.cloneValue(kv);
195           }
196           if (count == 1) {
197             secondValue = CellUtil.cloneValue(kv);
198           }
199           count++;
200           if (count == 2) {
201             break;
202           }
203         }
204 
205 
206         if (firstValue == null) {
207           throw new NullPointerException(Bytes.toString(r.getRow()) +
208             ": first value is null");
209         }
210         String first = Bytes.toString(firstValue);
211 
212         if (secondValue == null) {
213           throw new NullPointerException(Bytes.toString(r.getRow()) +
214             ": second value is null");
215         }
216         byte[] secondReversed = new byte[secondValue.length];
217         for (int i = 0, j = secondValue.length - 1; j >= 0; j--, i++) {
218           secondReversed[i] = secondValue[j];
219         }
220         String second = Bytes.toString(secondReversed);
221 
222         if (first.compareTo(second) != 0) {
223           if (getLog().isDebugEnabled()) {
224             getLog().debug("second key is not the reverse of first. row=" +
225                 Bytes.toStringBinary(r.getRow()) + ", first value=" + first +
226                 ", second value=" + second);
227           }
228           fail();
229         }
230       }
231     } finally {
232       scanner.close();
233     }
234   }
235 }