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.assertFalse;
22  import static org.junit.Assert.assertNotNull;
23  import static org.junit.Assert.assertTrue;
24  import static org.junit.Assert.fail;
25  
26  import java.io.File;
27  import java.io.IOException;
28  import java.util.Map;
29  import java.util.NavigableMap;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.hadoop.fs.FileUtil;
34  import org.apache.hadoop.fs.Path;
35  import org.apache.hadoop.hbase.TableName;
36  import org.apache.hadoop.hbase.TableNotEnabledException;
37  import org.apache.hadoop.hbase.TableNotFoundException;
38  import org.apache.hadoop.hbase.client.Admin;
39  import org.apache.hadoop.hbase.testclassification.LargeTests;
40  import org.apache.hadoop.hbase.client.HTable;
41  import org.apache.hadoop.hbase.client.Put;
42  import org.apache.hadoop.hbase.client.Result;
43  import org.apache.hadoop.hbase.client.Scan;
44  import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
45  import org.apache.hadoop.hbase.util.Bytes;
46  import org.apache.hadoop.mapreduce.Counter;
47  import org.apache.hadoop.mapreduce.Counters;
48  import org.apache.hadoop.mapreduce.Job;
49  import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
50  import org.junit.Test;
51  import org.junit.experimental.categories.Category;
52  
53  /**
54   * Test Map/Reduce job over HBase tables. The map/reduce process we're testing
55   * on our tables is simple - take every row in the table, reverse the value of
56   * a particular cell, and write it back to the table.
57   */
58  @Category(LargeTests.class)
59  public class TestTableMapReduce extends TestTableMapReduceBase {
60    private static final Log LOG = LogFactory.getLog(TestTableMapReduce.class);
61  
62    @Override
63    protected Log getLog() { return LOG; }
64  
65    /**
66     * Pass the given key and processed record reduce
67     */
68    static class ProcessContentsMapper extends TableMapper<ImmutableBytesWritable, Put> {
69  
70      /**
71       * Pass the key, and reversed value to reduce
72       *
73       * @param key
74       * @param value
75       * @param context
76       * @throws IOException
77       */
78      @Override
79      public void map(ImmutableBytesWritable key, Result value,
80        Context context)
81      throws IOException, InterruptedException {
82        if (value.size() != 1) {
83          throw new IOException("There should only be one input column");
84        }
85        Map<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>>
86          cf = value.getMap();
87        if(!cf.containsKey(INPUT_FAMILY)) {
88          throw new IOException("Wrong input columns. Missing: '" +
89            Bytes.toString(INPUT_FAMILY) + "'.");
90        }
91  
92        // Get the original value and reverse it
93        String originalValue = Bytes.toString(value.getValue(INPUT_FAMILY, INPUT_FAMILY));
94        StringBuilder newValue = new StringBuilder(originalValue);
95        newValue.reverse();
96        // Now set the value to be collected
97        Put outval = new Put(key.get());
98        outval.add(OUTPUT_FAMILY, null, Bytes.toBytes(newValue.toString()));
99        context.write(key, outval);
100     }
101   }
102 
103   protected void runTestOnTable(HTable table) throws IOException {
104     Job job = null;
105     try {
106       LOG.info("Before map/reduce startup");
107       job = new Job(table.getConfiguration(), "process column contents");
108       job.setNumReduceTasks(1);
109       Scan scan = new Scan();
110       scan.addFamily(INPUT_FAMILY);
111       TableMapReduceUtil.initTableMapperJob(
112         Bytes.toString(table.getTableName()), scan,
113         ProcessContentsMapper.class, ImmutableBytesWritable.class,
114         Put.class, job);
115       TableMapReduceUtil.initTableReducerJob(
116         Bytes.toString(table.getTableName()),
117         IdentityTableReducer.class, job);
118       FileOutputFormat.setOutputPath(job, new Path("test"));
119       LOG.info("Started " + Bytes.toString(table.getTableName()));
120       assertTrue(job.waitForCompletion(true));
121       LOG.info("After map/reduce completion");
122 
123       // verify map-reduce results
124       verify(table.getName());
125 
126       verifyJobCountersAreEmitted(job);
127     } catch (InterruptedException e) {
128       throw new IOException(e);
129     } catch (ClassNotFoundException e) {
130       throw new IOException(e);
131     } finally {
132       table.close();
133       if (job != null) {
134         FileUtil.fullyDelete(
135           new File(job.getConfiguration().get("hadoop.tmp.dir")));
136       }
137     }
138   }
139 
140   /**
141    * Verify scan counters are emitted from the job
142    * @param job
143    * @throws IOException
144    */
145   private void verifyJobCountersAreEmitted(Job job) throws IOException {
146     Counters counters = job.getCounters();
147     Counter counter
148       = counters.findCounter(TableRecordReaderImpl.HBASE_COUNTER_GROUP_NAME, "RPC_CALLS");
149     assertNotNull("Unable to find Job counter for HBase scan metrics, RPC_CALLS", counter);
150     assertTrue("Counter value for RPC_CALLS should be larger than 0", counter.getValue() > 0);
151   }
152 
153   @Test(expected = TableNotEnabledException.class)
154   public void testWritingToDisabledTable() throws IOException {
155 
156     try (Admin admin = UTIL.getConnection().getAdmin();
157         HTable table = new HTable(UTIL.getConfiguration(), TABLE_FOR_NEGATIVE_TESTS)) {
158       admin.disableTable(table.getName());
159       runTestOnTable(table);
160       fail("Should not have reached here, should have thrown an exception");
161     }
162   }
163 
164   @Test(expected = TableNotFoundException.class)
165   public void testWritingToNonExistentTable() throws IOException {
166 
167     try (HTable table = new HTable(UTIL.getConfiguration(),
168         TableName.valueOf("table-does-not-exist"))) {
169       runTestOnTable(table);
170       fail("Should not have reached here, should have thrown an exception");
171     }
172   }
173 }