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.client;
19  
20  import static org.apache.hadoop.hbase.client.ConnectionUtils.createScanResultCache;
21  
22  import java.io.IOException;
23  import java.io.InterruptedIOException;
24  import java.util.LinkedList;
25  import java.util.concurrent.ExecutorService;
26  
27  import org.apache.commons.lang.mutable.MutableBoolean;
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.apache.hadoop.conf.Configuration;
31  import org.apache.hadoop.hbase.DoNotRetryIOException;
32  import org.apache.hadoop.hbase.HConstants;
33  import org.apache.hadoop.hbase.HRegionInfo;
34  import org.apache.hadoop.hbase.regionserver.LeaseException;
35  import org.apache.hadoop.hbase.NotServingRegionException;
36  import org.apache.hadoop.hbase.TableName;
37  import org.apache.hadoop.hbase.UnknownScannerException;
38  import org.apache.hadoop.hbase.classification.InterfaceAudience;
39  import org.apache.hadoop.hbase.client.ScannerCallable.MoreResults;
40  import org.apache.hadoop.hbase.exceptions.OutOfOrderScannerNextException;
41  import org.apache.hadoop.hbase.exceptions.ScannerResetException;
42  import org.apache.hadoop.hbase.ipc.RpcControllerFactory;
43  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
44  import org.apache.hadoop.hbase.regionserver.RegionServerStoppedException;
45  import org.apache.hadoop.hbase.util.Bytes;
46  
47  /**
48   * Implements the scanner interface for the HBase client. If there are multiple regions in a table,
49   * this scanner will iterate through them all.
50   */
51  @InterfaceAudience.Private
52  public abstract class ClientScanner extends AbstractClientScanner {
53    private static final Log LOG = LogFactory.getLog(ClientScanner.class);
54  
55    protected final Scan scan;
56    protected boolean closed = false;
57    // Current region scanner is against. Gets cleared if current region goes
58    // wonky: e.g. if it splits on us.
59    protected HRegionInfo currentRegion = null;
60    protected ScannerCallableWithReplicas callable = null;
61    protected final LinkedList<Result> cache = new LinkedList<Result>();
62    private final ScanResultCache scanResultCache;
63    protected final int caching;
64    protected long lastNext;
65    // Keep lastResult returned successfully in case we have to reset scanner.
66    protected Result lastResult = null;
67    protected final long maxScannerResultSize;
68    private final ClusterConnection connection;
69    private final TableName tableName;
70    protected final int scannerTimeout;
71    protected boolean scanMetricsPublished = false;
72    protected RpcRetryingCaller<Result[]> caller;
73    protected RpcControllerFactory rpcControllerFactory;
74    protected Configuration conf;
75    // The timeout on the primary. Applicable if there are multiple replicas for a region
76    // In that case, we will only wait for this much timeout on the primary before going
77    // to the replicas and trying the same scan. Note that the retries will still happen
78    // on each replica and the first successful results will be taken. A timeout of 0 is
79    // disallowed.
80    protected final int primaryOperationTimeout;
81    private int retries;
82    protected final ExecutorService pool;
83  
84    /**
85     * Create a new ClientScanner for the specified table Note that the passed {@link Scan}'s start
86     * row maybe changed changed.
87     * @param conf The {@link Configuration} to use.
88     * @param scan {@link Scan} to use in this scanner
89     * @param tableName The table that we wish to scan
90     * @param connection Connection identifying the cluster
91     * @throws IOException
92     */
93    public ClientScanner(final Configuration conf, final Scan scan, final TableName tableName,
94        ClusterConnection connection, RpcRetryingCallerFactory rpcFactory,
95        RpcControllerFactory controllerFactory, ExecutorService pool, int primaryOperationTimeout)
96        throws IOException {
97      if (LOG.isTraceEnabled()) {
98        LOG.trace(
99          "Scan table=" + tableName + ", startRow=" + Bytes.toStringBinary(scan.getStartRow()));
100     }
101     this.scan = scan;
102     this.tableName = tableName;
103     this.lastNext = System.currentTimeMillis();
104     this.connection = connection;
105     this.pool = pool;
106     this.primaryOperationTimeout = primaryOperationTimeout;
107     this.retries = conf.getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER,
108       HConstants.DEFAULT_HBASE_CLIENT_RETRIES_NUMBER);
109     if (scan.getMaxResultSize() > 0) {
110       this.maxScannerResultSize = scan.getMaxResultSize();
111     } else {
112       this.maxScannerResultSize = conf.getLong(HConstants.HBASE_CLIENT_SCANNER_MAX_RESULT_SIZE_KEY,
113         HConstants.DEFAULT_HBASE_CLIENT_SCANNER_MAX_RESULT_SIZE);
114     }
115     this.scannerTimeout = conf.getInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD,
116         HConstants.DEFAULT_HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD);
117 
118     // check if application wants to collect scan metrics
119     initScanMetrics(scan);
120 
121     // Use the caching from the Scan. If not set, use the default cache setting for this table.
122     if (this.scan.getCaching() > 0) {
123       this.caching = this.scan.getCaching();
124     } else {
125       this.caching = conf.getInt(HConstants.HBASE_CLIENT_SCANNER_CACHING,
126         HConstants.DEFAULT_HBASE_CLIENT_SCANNER_CACHING);
127     }
128 
129     this.caller = rpcFactory.<Result[]> newCaller();
130     this.rpcControllerFactory = controllerFactory;
131 
132     this.conf = conf;
133     this.scanResultCache = createScanResultCache(scan, cache);
134   }
135 
136   protected ClusterConnection getConnection() {
137     return this.connection;
138   }
139 
140   /**
141    * @return Table name
142    * @deprecated As of release 0.96
143    *             (<a href="https://issues.apache.org/jira/browse/HBASE-9508">HBASE-9508</a>). This
144    *             will be removed in HBase 2.0.0. Use {@link #getTable()}.
145    */
146   @Deprecated
147   protected byte[] getTableName() {
148     return this.tableName.getName();
149   }
150 
151   protected TableName getTable() {
152     return this.tableName;
153   }
154 
155   protected int getRetries() {
156     return this.retries;
157   }
158 
159   protected int getScannerTimeout() {
160     return this.scannerTimeout;
161   }
162 
163   protected Configuration getConf() {
164     return this.conf;
165   }
166 
167   protected Scan getScan() {
168     return scan;
169   }
170 
171   protected ExecutorService getPool() {
172     return pool;
173   }
174 
175   protected int getPrimaryOperationTimeout() {
176     return primaryOperationTimeout;
177   }
178 
179   protected int getCaching() {
180     return caching;
181   }
182 
183   protected long getTimestamp() {
184     return lastNext;
185   }
186 
187   protected long getMaxResultSize() {
188     return maxScannerResultSize;
189   }
190 
191   private void closeScanner() throws IOException {
192     if (this.callable != null) {
193       this.callable.setClose();
194       call(callable, caller, scannerTimeout, false);
195       this.callable = null;
196     }
197   }
198 
199   /**
200    * Will be called in moveToNextRegion when currentRegion is null. Abstract because for normal
201    * scan, we will start next scan from the endKey of the currentRegion, and for reversed scan, we
202    * will start next scan from the startKey of the currentRegion.
203    * @return {@code false} if we have reached the stop row. Otherwise {@code true}.
204    */
205   protected abstract boolean setNewStartKey();
206 
207   /**
208    * Will be called in moveToNextRegion to create ScannerCallable. Abstract because for reversed
209    * scan we need to create a ReversedScannerCallable.
210    */
211   protected abstract ScannerCallable createScannerCallable();
212 
213   /**
214    * Close the previous scanner and create a new ScannerCallable for the next scanner.
215    * <p>
216    * Marked as protected only because TestClientScanner need to override this method.
217    * @return false if we should terminate the scan. Otherwise
218    */
219   protected boolean moveToNextRegion() {
220     // Close the previous scanner if it's open
221     try {
222       closeScanner();
223     } catch (IOException e) {
224       // not a big deal continue
225       if (LOG.isDebugEnabled()) {
226         LOG.debug("close scanner for " + currentRegion + " failed", e);
227       }
228     }
229     if (currentRegion != null) {
230       if (!setNewStartKey()) {
231         return false;
232       }
233       scan.resetMvccReadPoint();
234       if (LOG.isTraceEnabled()) {
235         LOG.trace("Finished " + this.currentRegion);
236       }
237     }
238     if (LOG.isDebugEnabled() && this.currentRegion != null) {
239       // Only worth logging if NOT first region in scan.
240       LOG.debug(
241         "Advancing internal scanner to startKey at '" + Bytes.toStringBinary(scan.getStartRow()) +
242             "', " + (scan.includeStartRow() ? "inclusive" : "exclusive"));
243     }
244     // clear the current region, we will set a new value to it after the first call of the new
245     // callable.
246     this.currentRegion = null;
247     this.callable =
248         new ScannerCallableWithReplicas(getTable(), getConnection(), createScannerCallable(), pool,
249             primaryOperationTimeout, scan, getRetries(), scannerTimeout, caching, conf, caller);
250     this.callable.setCaching(this.caching);
251     if (this.scanMetrics != null) {
252       this.scanMetrics.countOfRegions.incrementAndGet();
253     }
254     return true;
255   }
256 
257   boolean isAnyRPCcancelled() {
258     return callable.isAnyRPCcancelled();
259   }
260 
261   private Result[] call(ScannerCallableWithReplicas callable, RpcRetryingCaller<Result[]> caller,
262       int scannerTimeout, boolean updateCurrentRegion) throws IOException {
263     if (Thread.interrupted()) {
264       throw new InterruptedIOException();
265     }
266     // callWithoutRetries is at this layer. Within the ScannerCallableWithReplicas,
267     // we do a callWithRetries
268     Result[] rrs = caller.callWithoutRetries(callable, scannerTimeout);
269     if (currentRegion == null && updateCurrentRegion) {
270       currentRegion = callable.getHRegionInfo();
271     }
272     return rrs;
273   }
274 
275   /**
276    * Publish the scan metrics. For now, we use scan.setAttribute to pass the metrics back to the
277    * application or TableInputFormat.Later, we could push it to other systems. We don't use metrics
278    * framework because it doesn't support multi-instances of the same metrics on the same machine;
279    * for scan/map reduce scenarios, we will have multiple scans running at the same time. By
280    * default, scan metrics are disabled; if the application wants to collect them, this behavior can
281    * be turned on by calling calling {@link Scan#setScanMetricsEnabled(boolean)}
282    */
283   protected void writeScanMetrics() {
284     if (this.scanMetrics == null || scanMetricsPublished) {
285       return;
286     }
287     // Publish ScanMetrics to the Scan Object.
288     // As we have claimed in the comment of Scan.getScanMetrics, this relies on that user will not
289     // call ResultScanner.getScanMetrics and reset the ScanMetrics. Otherwise the metrics published
290     // to Scan will be messed up.
291     scan.setAttribute(Scan.SCAN_ATTRIBUTES_METRICS_DATA,
292       ProtobufUtil.toScanMetrics(scanMetrics, false).toByteArray());
293     scanMetricsPublished = true;
294   }
295 
296   @Override
297   public Result next() throws IOException {
298     // If the scanner is closed and there's nothing left in the cache, next is a no-op.
299     if (cache.size() == 0 && this.closed) {
300       return null;
301     }
302     if (cache.size() == 0) {
303       loadCache();
304     }
305 
306     if (cache.size() > 0) {
307       return cache.poll();
308     }
309 
310     // if we exhausted this scanner before calling close, write out the scan metrics
311     writeScanMetrics();
312     return null;
313   }
314 
315   public int getCacheSize() {
316     return cache != null ? cache.size() : 0;
317   }
318 
319   private boolean scanExhausted(Result[] values) {
320     return callable.moreResultsForScan() == MoreResults.NO;
321   }
322 
323   private boolean regionExhausted(Result[] values) {
324     // 1. Not a heartbeat message and we get nothing, this means the region is exhausted. And in the
325     // old time we always return empty result for a open scanner operation so we add a check here to
326     // keep compatible with the old logic. Should remove the isOpenScanner in the future.
327     // 2. Server tells us that it has no more results for this region.
328     return (values.length == 0 && !callable.isHeartbeatMessage()) ||
329         callable.moreResultsInRegion() == MoreResults.NO;
330   }
331 
332   private void closeScannerIfExhausted(boolean exhausted) throws IOException {
333     if (exhausted) {
334       closeScanner();
335     }
336   }
337 
338   private void handleScanError(DoNotRetryIOException e,
339       MutableBoolean retryAfterOutOfOrderException, int retriesLeft) throws DoNotRetryIOException {
340     // An exception was thrown which makes any partial results that we were collecting
341     // invalid. The scanner will need to be reset to the beginning of a row.
342     scanResultCache.clear();
343 
344     // Unfortunately, DNRIOE is used in two different semantics.
345     // (1) The first is to close the client scanner and bubble up the exception all the way
346     // to the application. This is preferred when the exception is really un-recoverable
347     // (like CorruptHFileException, etc). Plain DoNotRetryIOException also falls into this
348     // bucket usually.
349     // (2) Second semantics is to close the current region scanner only, but continue the
350     // client scanner by overriding the exception. This is usually UnknownScannerException,
351     // OutOfOrderScannerNextException, etc where the region scanner has to be closed, but the
352     // application-level ClientScanner has to continue without bubbling up the exception to
353     // the client. See RSRpcServices to see how it throws DNRIOE's.
354     // See also: HBASE-16604, HBASE-17187
355 
356     // If exception is any but the list below throw it back to the client; else setup
357     // the scanner and retry.
358     Throwable cause = e.getCause();
359     if ((cause != null && cause instanceof NotServingRegionException) ||
360         (cause != null && cause instanceof RegionServerStoppedException) ||
361         e instanceof OutOfOrderScannerNextException || e instanceof UnknownScannerException ||
362         e instanceof ScannerResetException || e instanceof LeaseException) {
363       // Pass. It is easier writing the if loop test as list of what is allowed rather than
364       // as a list of what is not allowed... so if in here, it means we do not throw.
365       if (retriesLeft <= 0) {
366         throw e; // no more retries
367       }
368     } else {
369       throw e;
370     }
371 
372     // Else, its signal from depths of ScannerCallable that we need to reset the scanner.
373     if (this.lastResult != null) {
374       // The region has moved. We need to open a brand new scanner at the new location.
375       // Reset the startRow to the row we've seen last so that the new scanner starts at
376       // the correct row. Otherwise we may see previously returned rows again.
377       // If the lastRow is not partial, then we should start from the next row. As now we can
378       // exclude the start row, the logic here is the same for both normal scan and reversed scan.
379       // If lastResult is partial then include it, otherwise exclude it.
380       scan.withStartRow(lastResult.getRow(), lastResult.mayHaveMoreCellsInRow());
381     }
382     if (e instanceof OutOfOrderScannerNextException) {
383       if (retryAfterOutOfOrderException.isTrue()) {
384         retryAfterOutOfOrderException.setValue(false);
385       } else {
386         // TODO: Why wrap this in a DNRIOE when it already is a DNRIOE?
387         throw new DoNotRetryIOException(
388             "Failed after retry of OutOfOrderScannerNextException: was there a rpc timeout?", e);
389       }
390     }
391     // Clear region.
392     this.currentRegion = null;
393     // Set this to zero so we don't try and do an rpc and close on remote server when
394     // the exception we got was UnknownScanner or the Server is going down.
395     callable = null;
396   }
397 
398   /**
399    * Contact the servers to load more {@link Result}s in the cache.
400    */
401   protected void loadCache() throws IOException {
402     // check if scanner was closed during previous prefetch
403     if (closed) {
404       return;
405     }
406     long remainingResultSize = maxScannerResultSize;
407     int countdown = this.caching;
408     // This is possible if we just stopped at the boundary of a region in the previous call.
409     if (callable == null) {
410       if (!moveToNextRegion()) {
411         return;
412       }
413     }
414     // This flag is set when we want to skip the result returned. We do
415     // this when we reset scanner because it split under us.
416     MutableBoolean retryAfterOutOfOrderException = new MutableBoolean(true);
417     // Even if we are retrying due to UnknownScannerException, ScannerResetException, etc. we should
418     // make sure that we are not retrying indefinitely.
419     int retriesLeft = getRetries();
420     for (;;) {
421       Result[] values;
422       try {
423         // Server returns a null values if scanning is to stop. Else,
424         // returns an empty array if scanning is to go on and we've just
425         // exhausted current region.
426         // now we will also fetch data when openScanner, so do not make a next call again if values
427         // is already non-null.
428         values = call(callable, caller, scannerTimeout, true);
429         // When the replica switch happens, we need to do certain operations again.
430         // The callable will openScanner with the right startkey but we need to pick up
431         // from there. Bypass the rest of the loop and let the catch-up happen in the beginning
432         // of the loop as it happens for the cases where we see exceptions.
433         if (callable.switchedToADifferentReplica()) {
434           // Any accumulated partial results are no longer valid since the callable will
435           // openScanner with the correct startkey and we must pick up from there
436           scanResultCache.clear();
437           this.currentRegion = callable.getHRegionInfo();
438         }
439         retryAfterOutOfOrderException.setValue(true);
440       } catch (DoNotRetryIOException e) {
441         handleScanError(e, retryAfterOutOfOrderException, retriesLeft--);
442         // reopen the scanner
443         if (!moveToNextRegion()) {
444           break;
445         }
446         continue;
447       }
448       long currentTime = System.currentTimeMillis();
449       if (this.scanMetrics != null) {
450         this.scanMetrics.sumOfMillisSecBetweenNexts.addAndGet(currentTime - lastNext);
451       }
452       lastNext = currentTime;
453       // Groom the array of Results that we received back from the server before adding that
454       // Results to the scanner's cache. If partial results are not allowed to be seen by the
455       // caller, all book keeping will be performed within this method.
456       int numberOfCompleteRowsBefore = scanResultCache.numberOfCompleteRows();
457       scanResultCache.loadResultsToCache(values, callable.isHeartbeatMessage());
458       int numberOfCompleteRows =
459           scanResultCache.numberOfCompleteRows() - numberOfCompleteRowsBefore;
460       if (scanResultCache.getCount() > 0) {
461         remainingResultSize -= scanResultCache.getResultSize();
462         scanResultCache.resetResultSize();
463         countdown -= scanResultCache.getCount();
464         scanResultCache.resetCount();
465         this.lastResult = scanResultCache.getLastResult();
466       }
467 
468       if (scan.getLimit() > 0) {
469         int newLimit = scan.getLimit() - numberOfCompleteRows;
470         assert newLimit >= 0;
471         scan.setLimit(newLimit);
472       }
473       if (scanExhausted(values)) {
474         closeScanner();
475         closed = true;
476         break;
477       }
478       boolean regionExhausted = regionExhausted(values);
479       if (callable.isHeartbeatMessage()) {
480         if (!cache.isEmpty()) {
481           // Caller of this method just wants a Result. If we see a heartbeat message, it means
482           // processing of the scan is taking a long time server side. Rather than continue to
483           // loop until a limit (e.g. size or caching) is reached, break out early to avoid causing
484           // unnecesary delays to the caller
485           if (LOG.isTraceEnabled()) {
486             LOG.trace("Heartbeat message received and cache contains Results." +
487                 " Breaking out of scan loop");
488           }
489           // we know that the region has not been exhausted yet so just break without calling
490           // closeScannerIfExhausted
491           break;
492         }
493       }
494       if (cache.isEmpty() && !closed && scan.isNeedCursorResult()) {
495         if (callable.isHeartbeatMessage() && callable.getCursor() != null) {
496           // Use cursor row key from server
497           cache.add(Result.createCursorResult(callable.getCursor()));
498           break;
499         }
500         if (values.length > 0) {
501           // It is size limit exceed and we need return the last Result's row.
502           // When user setBatch and the scanner is reopened, the server may return Results that
503           // user has seen and the last Result can not be seen because the number is not enough.
504           // So the row keys of results may not be same, we must use the last one.
505           cache.add(Result.createCursorResult(new Cursor(values[values.length - 1].getRow())));
506           break;
507         }
508       }
509       if (countdown <= 0) {
510         // we have enough result.
511         closeScannerIfExhausted(regionExhausted);
512         break;
513       }
514       if (remainingResultSize <= 0) {
515         if (!cache.isEmpty()) {
516           closeScannerIfExhausted(regionExhausted);
517           break;
518         } else {
519           // we have reached the max result size but we still can not find anything to return to the
520           // user. Reset the maxResultSize and try again.
521           remainingResultSize = maxScannerResultSize;
522         }
523       }
524       // we are done with the current region
525       if (regionExhausted) {
526         if (!moveToNextRegion()) {
527           break;
528         }
529       }
530     }
531   }
532 
533   @Override
534   public void close() {
535     if (!scanMetricsPublished) writeScanMetrics();
536     if (callable != null) {
537       callable.setClose();
538       try {
539         call(callable, caller, scannerTimeout, false);
540       } catch (UnknownScannerException e) {
541         // We used to catch this error, interpret, and rethrow. However, we
542         // have since decided that it's not nice for a scanner's close to
543         // throw exceptions. Chances are it was just due to lease time out.
544       } catch (IOException e) {
545         /* An exception other than UnknownScanner is unexpected. */
546         LOG.warn("scanner failed to close. Exception follows: " + e);
547       }
548       callable = null;
549     }
550     closed = true;
551   }
552 
553   @Override
554   public boolean renewLease() {
555     if (callable != null) {
556       // do not return any rows, do not advance the scanner
557       callable.setRenew(true);
558       try {
559         this.caller.callWithoutRetries(callable, this.scannerTimeout);
560       } catch (Exception e) {
561         return false;
562       } finally {
563         callable.setRenew(false);
564       }
565       return true;
566     }
567     return false;
568   }
569 }