View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.mapreduce;
19  
20  import java.io.IOException;
21  import java.lang.reflect.Method;
22  import java.util.Map;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.hadoop.hbase.classification.InterfaceAudience;
27  import org.apache.hadoop.hbase.classification.InterfaceStability;
28  import org.apache.hadoop.conf.Configuration;
29  import org.apache.hadoop.hbase.client.HTable;
30  import org.apache.hadoop.hbase.client.Result;
31  import org.apache.hadoop.hbase.client.ResultScanner;
32  import org.apache.hadoop.hbase.client.Scan;
33  import org.apache.hadoop.hbase.client.ScannerCallable;
34  import org.apache.hadoop.hbase.client.Table;
35  import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
36  import org.apache.hadoop.hbase.DoNotRetryIOException;
37  import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
38  import org.apache.hadoop.hbase.util.Bytes;
39  import org.apache.hadoop.mapreduce.Counter;
40  import org.apache.hadoop.mapreduce.InputSplit;
41  import org.apache.hadoop.mapreduce.TaskAttemptContext;
42  import org.apache.hadoop.util.StringUtils;
43  
44  /**
45   * Iterate over an HBase table data, return (ImmutableBytesWritable, Result)
46   * pairs.
47   */
48  @InterfaceAudience.Public
49  @InterfaceStability.Stable
50  public class TableRecordReaderImpl {
51    public static final String LOG_PER_ROW_COUNT
52      = "hbase.mapreduce.log.scanner.rowcount";
53  
54    private static final Log LOG = LogFactory.getLog(TableRecordReaderImpl.class);
55  
56    // HBASE_COUNTER_GROUP_NAME is the name of mapreduce counter group for HBase
57    private static final String HBASE_COUNTER_GROUP_NAME =
58      "HBase Counters";
59    private ResultScanner scanner = null;
60    private Scan scan = null;
61    private Scan currentScan = null;
62    private Table htable = null;
63    private byte[] lastSuccessfulRow = null;
64    private ImmutableBytesWritable key = null;
65    private Result value = null;
66    private TaskAttemptContext context = null;
67    private Method getCounter = null;
68    private long numRestarts = 0;
69    private long numStale = 0;
70    private long timestamp;
71    private int rowcount;
72    private boolean logScannerActivity = false;
73    private int logPerRowCount = 100;
74  
75    /**
76     * Restart from survivable exceptions by creating a new scanner.
77     *
78     * @param firstRow  The first row to start at.
79     * @throws IOException When restarting fails.
80     */
81    public void restart(byte[] firstRow) throws IOException {
82      currentScan = new Scan(scan);
83      currentScan.setStartRow(firstRow);
84      currentScan.setScanMetricsEnabled(true);
85      if (this.scanner != null) {
86        if (logScannerActivity) {
87          LOG.info("Closing the previously opened scanner object.");
88        }
89        this.scanner.close();
90      }
91      this.scanner = this.htable.getScanner(currentScan);
92      if (logScannerActivity) {
93        LOG.info("Current scan=" + currentScan.toString());
94        timestamp = System.currentTimeMillis();
95        rowcount = 0;
96      }
97    }
98  
99    /**
100    * In new mapreduce APIs, TaskAttemptContext has two getCounter methods
101    * Check if getCounter(String, String) method is available.
102    * @return The getCounter method or null if not available.
103    * @throws IOException
104    */
105   protected static Method retrieveGetCounterWithStringsParams(TaskAttemptContext context)
106   throws IOException {
107     Method m = null;
108     try {
109       m = context.getClass().getMethod("getCounter",
110         new Class [] {String.class, String.class});
111     } catch (SecurityException e) {
112       throw new IOException("Failed test for getCounter", e);
113     } catch (NoSuchMethodException e) {
114       // Ignore
115     }
116     return m;
117   }
118 
119   /**
120    * Sets the HBase table.
121    *
122    * @param htable  The {@link org.apache.hadoop.hbase.HTableDescriptor} to scan.
123    */
124   public void setHTable(Table htable) {
125     Configuration conf = htable.getConfiguration();
126     logScannerActivity = conf.getBoolean(
127       ScannerCallable.LOG_SCANNER_ACTIVITY, false);
128     logPerRowCount = conf.getInt(LOG_PER_ROW_COUNT, 100);
129     this.htable = htable;
130   }
131 
132   /**
133    * Sets the HBase table.
134    *
135    * @param htable  The {@link org.apache.hadoop.hbase.client.HTable} to scan.
136    * @deprecated Use {@link #setHTable(org.apache.hadoop.hbase.client.HTable htable)} instead.
137    */
138   @Deprecated
139   public void setHTable(HTable htable) {
140     Configuration conf = htable.getConfiguration();
141     logScannerActivity = conf.getBoolean(
142         ScannerCallable.LOG_SCANNER_ACTIVITY, false);
143     logPerRowCount = conf.getInt(LOG_PER_ROW_COUNT, 100);
144     this.htable = htable;
145   }
146 
147   /**
148    * Sets the scan defining the actual details like columns etc.
149    *
150    * @param scan  The scan to set.
151    */
152   public void setScan(Scan scan) {
153     this.scan = scan;
154   }
155 
156   /**
157    * Build the scanner. Not done in constructor to allow for extension.
158    *
159    * @throws IOException
160    * @throws InterruptedException
161    */
162   public void initialize(InputSplit inputsplit,
163       TaskAttemptContext context) throws IOException,
164       InterruptedException {
165     if (context != null) {
166       this.context = context;
167       getCounter = retrieveGetCounterWithStringsParams(context);
168     }
169     restart(scan.getStartRow());
170   }
171 
172   /**
173    * Closes the split.
174    *
175    *
176    */
177   public void close() {
178     this.scanner.close();
179     try {
180       this.htable.close();
181     } catch (IOException ioe) {
182       LOG.warn("Error closing table", ioe);
183     }
184   }
185 
186   /**
187    * Returns the current key.
188    *
189    * @return The current key.
190    * @throws IOException
191    * @throws InterruptedException When the job is aborted.
192    */
193   public ImmutableBytesWritable getCurrentKey() throws IOException,
194       InterruptedException {
195     return key;
196   }
197 
198   /**
199    * Returns the current value.
200    *
201    * @return The current value.
202    * @throws IOException When the value is faulty.
203    * @throws InterruptedException When the job is aborted.
204    */
205   public Result getCurrentValue() throws IOException, InterruptedException {
206     return value;
207   }
208 
209 
210   /**
211    * Positions the record reader to the next record.
212    *
213    * @return <code>true</code> if there was another record.
214    * @throws IOException When reading the record failed.
215    * @throws InterruptedException When the job was aborted.
216    */
217   public boolean nextKeyValue() throws IOException, InterruptedException {
218     if (key == null) key = new ImmutableBytesWritable();
219     if (value == null) value = new Result();
220     try {
221       try {
222         value = this.scanner.next();
223         if (value != null && value.isStale()) numStale++;
224         if (logScannerActivity) {
225           rowcount ++;
226           if (rowcount >= logPerRowCount) {
227             long now = System.currentTimeMillis();
228             LOG.info("Mapper took " + (now-timestamp)
229               + "ms to process " + rowcount + " rows");
230             timestamp = now;
231             rowcount = 0;
232           }
233         }
234       } catch (IOException e) {
235         // do not retry if the exception tells us not to do so
236         if (e instanceof DoNotRetryIOException) {
237           throw e;
238         }
239         // try to handle all other IOExceptions by restarting
240         // the scanner, if the second call fails, it will be rethrown
241         LOG.info("recovered from " + StringUtils.stringifyException(e));
242         if (lastSuccessfulRow == null) {
243           LOG.warn("We are restarting the first next() invocation," +
244               " if your mapper has restarted a few other times like this" +
245               " then you should consider killing this job and investigate" +
246               " why it's taking so long.");
247         }
248         if (lastSuccessfulRow == null) {
249           restart(scan.getStartRow());
250         } else {
251           restart(lastSuccessfulRow);
252           scanner.next();    // skip presumed already mapped row
253         }
254         value = scanner.next();
255         if (value != null && value.isStale()) numStale++;
256         numRestarts++;
257       }
258       if (value != null && value.size() > 0) {
259         key.set(value.getRow());
260         lastSuccessfulRow = key.get();
261         return true;
262       }
263 
264       updateCounters();
265       return false;
266     } catch (IOException ioe) {
267       if (logScannerActivity) {
268         long now = System.currentTimeMillis();
269         LOG.info("Mapper took " + (now-timestamp)
270           + "ms to process " + rowcount + " rows");
271         LOG.info(ioe);
272         String lastRow = lastSuccessfulRow == null ?
273           "null" : Bytes.toStringBinary(lastSuccessfulRow);
274         LOG.info("lastSuccessfulRow=" + lastRow);
275       }
276       throw ioe;
277     }
278   }
279 
280   /**
281    * If hbase runs on new version of mapreduce, RecordReader has access to
282    * counters thus can update counters based on scanMetrics.
283    * If hbase runs on old version of mapreduce, it won't be able to get
284    * access to counters and TableRecorderReader can't update counter values.
285    * @throws IOException
286    */
287   private void updateCounters() throws IOException {
288     ScanMetrics scanMetrics = this.scan.getScanMetrics();
289     if (scanMetrics == null) {
290       return;
291     }
292 
293     updateCounters(scanMetrics, numRestarts, getCounter, context, numStale);
294   }
295 
296   protected static void updateCounters(ScanMetrics scanMetrics, long numScannerRestarts,
297       Method getCounter, TaskAttemptContext context, long numStale) {
298     // we can get access to counters only if hbase uses new mapreduce APIs
299     if (getCounter == null) {
300       return;
301     }
302 
303     try {
304       for (Map.Entry<String, Long> entry:scanMetrics.getMetricsMap().entrySet()) {
305         Counter ct = (Counter)getCounter.invoke(context,
306             HBASE_COUNTER_GROUP_NAME, entry.getKey());
307 
308         ct.increment(entry.getValue());
309       }
310       ((Counter) getCounter.invoke(context, HBASE_COUNTER_GROUP_NAME,
311           "NUM_SCANNER_RESTARTS")).increment(numScannerRestarts);
312       ((Counter) getCounter.invoke(context, HBASE_COUNTER_GROUP_NAME,
313           "NUM_SCAN_RESULTS_STALE")).increment(numStale);
314     } catch (Exception e) {
315       LOG.debug("can't update counter." + StringUtils.stringifyException(e));
316     }
317   }
318 
319   /**
320    * @deprecated Use {@link #updateCounters(ScanMetrics, long, Method, TaskAttemptContext, long)}
321    * instead.
322    */
323   @Deprecated
324   protected static void updateCounters(ScanMetrics scanMetrics, long numScannerRestarts,
325                                        Method getCounter, TaskAttemptContext context) {
326     // we can get access to counters only if hbase uses new mapreduce APIs
327     if (getCounter == null) {
328       return;
329     }
330 
331     try {
332       for (Map.Entry<String, Long> entry:scanMetrics.getMetricsMap().entrySet()) {
333         Counter ct = (Counter)getCounter.invoke(context,
334             HBASE_COUNTER_GROUP_NAME, entry.getKey());
335 
336         ct.increment(entry.getValue());
337       }
338       ((Counter) getCounter.invoke(context, HBASE_COUNTER_GROUP_NAME,
339           "NUM_SCANNER_RESTARTS")).increment(numScannerRestarts);
340     } catch (Exception e) {
341       LOG.debug("can't update counter." + StringUtils.stringifyException(e));
342     }
343   }
344 
345   /**
346    * The current progress of the record reader through its data.
347    *
348    * @return A number between 0.0 and 1.0, the fraction of the data read.
349    */
350   public float getProgress() {
351     // Depends on the total number of tuples
352     return 0;
353   }
354 
355 }