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