001/**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *     http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package org.apache.hadoop.hbase.mapreduce;
020
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertNotEquals;
023import static org.junit.Assert.assertTrue;
024
025import java.io.IOException;
026import java.util.*;
027
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.Path;
030import org.apache.hadoop.hbase.HBaseTestingUtility;
031import org.apache.hadoop.hbase.TableName;
032import org.apache.hadoop.hbase.client.Result;
033import org.apache.hadoop.hbase.client.Scan;
034import org.apache.hadoop.hbase.client.Table;
035import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
036import org.apache.hadoop.hbase.util.Bytes;
037import org.apache.hadoop.io.NullWritable;
038import org.apache.hadoop.mapred.JobConf;
039import org.apache.hadoop.mapreduce.InputSplit;
040import org.apache.hadoop.mapreduce.Job;
041import org.apache.hadoop.mapreduce.Reducer;
042import org.apache.hadoop.mapreduce.TaskCounter;
043import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
044import org.apache.hadoop.mapreduce.lib.output.NullOutputFormat;
045import org.junit.AfterClass;
046import org.junit.Assert;
047import org.junit.BeforeClass;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051
052/**
053 * <p>
054 * Tests various scan start and stop row scenarios. This is set in a scan and
055 * tested in a MapReduce job to see if that is handed over and done properly
056 * too.
057 * </p>
058 * <p>
059 * This test is broken into two parts in order to side-step the test timeout
060 * period of 900, as documented in HBASE-8326.
061 * </p>
062 */
063public abstract class TestTableInputFormatScanBase {
064
065  private static final Logger LOG = LoggerFactory.getLogger(TestTableInputFormatScanBase.class);
066  static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
067
068  static final TableName TABLE_NAME = TableName.valueOf("scantest");
069  static final byte[][] INPUT_FAMILYS = {Bytes.toBytes("content1"), Bytes.toBytes("content2")};
070  static final String KEY_STARTROW = "startRow";
071  static final String KEY_LASTROW = "stpRow";
072
073  private static Table table = null;
074
075  @BeforeClass
076  public static void setUpBeforeClass() throws Exception {
077    // test intermittently fails under hadoop2 (2.0.2-alpha) if shortcircuit-read (scr) is on.
078    // this turns it off for this test.  TODO: Figure out why scr breaks recovery.
079    System.setProperty("hbase.tests.use.shortcircuit.reads", "false");
080
081    // switch TIF to log at DEBUG level
082    TEST_UTIL.enableDebug(TableInputFormat.class);
083    TEST_UTIL.enableDebug(TableInputFormatBase.class);
084    // start mini hbase cluster
085    TEST_UTIL.startMiniCluster(3);
086    // create and fill table
087    table = TEST_UTIL.createMultiRegionTable(TABLE_NAME, INPUT_FAMILYS);
088    TEST_UTIL.loadTable(table, INPUT_FAMILYS, null, false);
089  }
090
091  @AfterClass
092  public static void tearDownAfterClass() throws Exception {
093    TEST_UTIL.shutdownMiniCluster();
094  }
095
096  /**
097   * Pass the key and value to reduce.
098   */
099  public static class ScanMapper
100  extends TableMapper<ImmutableBytesWritable, ImmutableBytesWritable> {
101
102    /**
103     * Pass the key and value to reduce.
104     *
105     * @param key  The key, here "aaa", "aab" etc.
106     * @param value  The value is the same as the key.
107     * @param context  The task context.
108     * @throws IOException When reading the rows fails.
109     */
110    @Override
111    public void map(ImmutableBytesWritable key, Result value,
112      Context context)
113    throws IOException, InterruptedException {
114      if (value.size() != 2) {
115        throw new IOException("There should be two input columns");
116      }
117      Map<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>>
118        cfMap = value.getMap();
119
120      if (!cfMap.containsKey(INPUT_FAMILYS[0]) || !cfMap.containsKey(INPUT_FAMILYS[1])) {
121        throw new IOException("Wrong input columns. Missing: '" +
122          Bytes.toString(INPUT_FAMILYS[0]) + "' or '" + Bytes.toString(INPUT_FAMILYS[1]) + "'.");
123      }
124
125      String val0 = Bytes.toStringBinary(value.getValue(INPUT_FAMILYS[0], null));
126      String val1 = Bytes.toStringBinary(value.getValue(INPUT_FAMILYS[1], null));
127      LOG.info("map: key -> " + Bytes.toStringBinary(key.get()) +
128               ", value -> (" + val0 + ", " + val1 + ")");
129      context.write(key, key);
130    }
131  }
132
133  /**
134   * Checks the last and first key seen against the scanner boundaries.
135   */
136  public static class ScanReducer
137  extends Reducer<ImmutableBytesWritable, ImmutableBytesWritable,
138                    NullWritable, NullWritable> {
139
140    private String first = null;
141    private String last = null;
142
143    protected void reduce(ImmutableBytesWritable key,
144        Iterable<ImmutableBytesWritable> values, Context context)
145    throws IOException ,InterruptedException {
146      int count = 0;
147      for (ImmutableBytesWritable value : values) {
148        String val = Bytes.toStringBinary(value.get());
149        LOG.info("reduce: key[" + count + "] -> " +
150          Bytes.toStringBinary(key.get()) + ", value -> " + val);
151        if (first == null) first = val;
152        last = val;
153        count++;
154      }
155    }
156
157    protected void cleanup(Context context)
158    throws IOException, InterruptedException {
159      Configuration c = context.getConfiguration();
160      String startRow = c.get(KEY_STARTROW);
161      String lastRow = c.get(KEY_LASTROW);
162      LOG.info("cleanup: first -> \"" + first + "\", start row -> \"" + startRow + "\"");
163      LOG.info("cleanup: last -> \"" + last + "\", last row -> \"" + lastRow + "\"");
164      if (startRow != null && startRow.length() > 0) {
165        assertEquals(startRow, first);
166      }
167      if (lastRow != null && lastRow.length() > 0) {
168        assertEquals(lastRow, last);
169      }
170    }
171
172  }
173
174  /**
175   * Tests an MR Scan initialized from properties set in the Configuration.
176   *
177   * @throws IOException
178   * @throws ClassNotFoundException
179   * @throws InterruptedException
180   */
181  protected void testScanFromConfiguration(String start, String stop, String last)
182  throws IOException, InterruptedException, ClassNotFoundException {
183    String jobName = "ScanFromConfig" + (start != null ? start.toUpperCase(Locale.ROOT) : "Empty") +
184      "To" + (stop != null ? stop.toUpperCase(Locale.ROOT) : "Empty");
185    Configuration c = new Configuration(TEST_UTIL.getConfiguration());
186    c.set(TableInputFormat.INPUT_TABLE, TABLE_NAME.getNameAsString());
187    c.set(TableInputFormat.SCAN_COLUMN_FAMILY, Bytes.toString(INPUT_FAMILYS[0]) + ", "
188          + Bytes.toString(INPUT_FAMILYS[1]));
189    c.set(KEY_STARTROW, start != null ? start : "");
190    c.set(KEY_LASTROW, last != null ? last : "");
191
192    if (start != null) {
193      c.set(TableInputFormat.SCAN_ROW_START, start);
194    }
195
196    if (stop != null) {
197      c.set(TableInputFormat.SCAN_ROW_STOP, stop);
198    }
199
200    Job job = new Job(c, jobName);
201    job.setMapperClass(ScanMapper.class);
202    job.setReducerClass(ScanReducer.class);
203    job.setMapOutputKeyClass(ImmutableBytesWritable.class);
204    job.setMapOutputValueClass(ImmutableBytesWritable.class);
205    job.setInputFormatClass(TableInputFormat.class);
206    job.setNumReduceTasks(1);
207    FileOutputFormat.setOutputPath(job, new Path(job.getJobName()));
208    TableMapReduceUtil.addDependencyJars(job);
209    assertTrue(job.waitForCompletion(true));
210  }
211
212  /**
213   * Tests a MR scan using specific start and stop rows.
214   *
215   * @throws IOException
216   * @throws ClassNotFoundException
217   * @throws InterruptedException
218   */
219  protected void testScan(String start, String stop, String last)
220  throws IOException, InterruptedException, ClassNotFoundException {
221    String jobName = "Scan" + (start != null ? start.toUpperCase(Locale.ROOT) : "Empty") +
222      "To" + (stop != null ? stop.toUpperCase(Locale.ROOT) : "Empty");
223    LOG.info("Before map/reduce startup - job " + jobName);
224    Configuration c = new Configuration(TEST_UTIL.getConfiguration());
225    Scan scan = new Scan();
226    scan.addFamily(INPUT_FAMILYS[0]);
227    scan.addFamily(INPUT_FAMILYS[1]);
228    if (start != null) {
229      scan.setStartRow(Bytes.toBytes(start));
230    }
231    c.set(KEY_STARTROW, start != null ? start : "");
232    if (stop != null) {
233      scan.setStopRow(Bytes.toBytes(stop));
234    }
235    c.set(KEY_LASTROW, last != null ? last : "");
236    LOG.info("scan before: " + scan);
237    Job job = new Job(c, jobName);
238    TableMapReduceUtil.initTableMapperJob(
239      TABLE_NAME, scan, ScanMapper.class,
240      ImmutableBytesWritable.class, ImmutableBytesWritable.class, job);
241    job.setReducerClass(ScanReducer.class);
242    job.setNumReduceTasks(1); // one to get final "first" and "last" key
243    FileOutputFormat.setOutputPath(job, new Path(job.getJobName()));
244    LOG.info("Started " + job.getJobName());
245    assertTrue(job.waitForCompletion(true));
246    LOG.info("After map/reduce completion - job " + jobName);
247  }
248
249
250  /**
251   * Tests Number of inputSplits for MR job when specify number of mappers for TableInputFormatXXX
252   * This test does not run MR job
253   *
254   * @throws IOException
255   * @throws ClassNotFoundException
256   * @throws InterruptedException
257   */
258  public void testNumOfSplits(int splitsPerRegion, int expectedNumOfSplits) throws IOException,
259      InterruptedException,
260      ClassNotFoundException {
261    String jobName = "TestJobForNumOfSplits";
262    LOG.info("Before map/reduce startup - job " + jobName);
263    Configuration c = new Configuration(TEST_UTIL.getConfiguration());
264    Scan scan = new Scan();
265    scan.addFamily(INPUT_FAMILYS[0]);
266    scan.addFamily(INPUT_FAMILYS[1]);
267    c.setInt("hbase.mapreduce.tableinput.mappers.per.region", splitsPerRegion);
268    c.set(KEY_STARTROW, "");
269    c.set(KEY_LASTROW, "");
270    Job job = new Job(c, jobName);
271    TableMapReduceUtil.initTableMapperJob(TABLE_NAME.getNameAsString(), scan, ScanMapper.class,
272        ImmutableBytesWritable.class, ImmutableBytesWritable.class, job);
273    TableInputFormat tif = new TableInputFormat();
274    tif.setConf(job.getConfiguration());
275    Assert.assertEquals(TABLE_NAME, table.getName());
276    List<InputSplit> splits = tif.getSplits(job);
277    Assert.assertEquals(expectedNumOfSplits, splits.size());
278  }
279
280  /**
281   * Run MR job to check the number of mapper = expectedNumOfSplits
282   * @throws IOException
283   * @throws InterruptedException
284   * @throws ClassNotFoundException
285   */
286  public void testNumOfSplitsMR(int splitsPerRegion, int expectedNumOfSplits) throws IOException,
287      InterruptedException,
288      ClassNotFoundException {
289    String jobName = "TestJobForNumOfSplits-MR";
290    LOG.info("Before map/reduce startup - job " + jobName);
291    JobConf c = new JobConf(TEST_UTIL.getConfiguration());
292    Scan scan = new Scan();
293    scan.addFamily(INPUT_FAMILYS[0]);
294    scan.addFamily(INPUT_FAMILYS[1]);
295    c.setInt("hbase.mapreduce.tableinput.mappers.per.region", splitsPerRegion);
296    c.set(KEY_STARTROW, "");
297    c.set(KEY_LASTROW, "");
298    Job job = Job.getInstance(c, jobName);
299    TableMapReduceUtil.initTableMapperJob(TABLE_NAME.getNameAsString(), scan, ScanMapper.class,
300        ImmutableBytesWritable.class, ImmutableBytesWritable.class, job);
301    job.setReducerClass(ScanReducer.class);
302    job.setNumReduceTasks(1);
303    job.setOutputFormatClass(NullOutputFormat.class);
304    assertTrue("job failed!", job.waitForCompletion(true));
305    // for some reason, hbase does not expose JobCounter.TOTAL_LAUNCHED_MAPS,
306    // we use TaskCounter.SHUFFLED_MAPS to get total launched maps
307    assertEquals("Saw the wrong count of mappers per region", expectedNumOfSplits,
308        job.getCounters().findCounter(TaskCounter.SHUFFLED_MAPS).getValue());
309  }
310
311  /**
312   * Run MR job to test autobalance for setting number of mappers for TIF
313   * This does not run real MR job
314   */
315  public void testAutobalanceNumOfSplit() throws IOException {
316    // set up splits for testing
317    List<InputSplit> splits = new ArrayList<>(5);
318    int[] regionLen = {10, 20, 20, 40, 60};
319    for (int i = 0; i < 5; i++) {
320      InputSplit split = new TableSplit(TABLE_NAME, new Scan(),
321          Bytes.toBytes(i), Bytes.toBytes(i + 1), "", "", regionLen[i] * 1048576);
322      splits.add(split);
323    }
324    TableInputFormat tif = new TableInputFormat();
325    List<InputSplit> res = tif.calculateAutoBalancedSplits(splits, 1073741824);
326
327    assertEquals("Saw the wrong number of splits", 5, res.size());
328    TableSplit ts1 = (TableSplit) res.get(0);
329    assertEquals("The first split end key should be", 2, Bytes.toInt(ts1.getEndRow()));
330    TableSplit ts2 = (TableSplit) res.get(1);
331    assertEquals("The second split regionsize should be", 20 * 1048576, ts2.getLength());
332    TableSplit ts3 = (TableSplit) res.get(2);
333    assertEquals("The third split start key should be", 3, Bytes.toInt(ts3.getStartRow()));
334    TableSplit ts4 = (TableSplit) res.get(4);
335    assertNotEquals("The seventh split start key should not be", 4, Bytes.toInt(ts4.getStartRow()));
336  }
337}
338