001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.client;
019
020import static org.apache.hadoop.hbase.client.ConnectionUtils.calcEstimatedSize;
021import static org.apache.hadoop.hbase.client.ConnectionUtils.createScanResultCache;
022import static org.apache.hadoop.hbase.client.ConnectionUtils.incRegionCountMetrics;
023
024import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
025
026import java.io.IOException;
027import java.io.InterruptedIOException;
028import java.util.ArrayDeque;
029import java.util.Queue;
030import java.util.concurrent.ExecutorService;
031
032import org.apache.commons.lang3.mutable.MutableBoolean;
033import org.apache.hadoop.conf.Configuration;
034import org.apache.hadoop.hbase.client.ScannerCallable.MoreResults;
035import org.apache.hadoop.hbase.DoNotRetryIOException;
036import org.apache.hadoop.hbase.exceptions.OutOfOrderScannerNextException;
037import org.apache.hadoop.hbase.exceptions.ScannerResetException;
038import org.apache.hadoop.hbase.HBaseConfiguration;
039import org.apache.hadoop.hbase.HConstants;
040import org.apache.hadoop.hbase.HRegionInfo;
041import org.apache.hadoop.hbase.ipc.RpcControllerFactory;
042import org.apache.hadoop.hbase.NotServingRegionException;
043import org.apache.hadoop.hbase.regionserver.RegionServerStoppedException;
044import org.apache.hadoop.hbase.TableName;
045import org.apache.hadoop.hbase.UnknownScannerException;
046import org.apache.hadoop.hbase.util.Bytes;
047import org.apache.yetus.audience.InterfaceAudience;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
052
053/**
054 * Implements the scanner interface for the HBase client. If there are multiple regions in a table,
055 * this scanner will iterate through them all.
056 */
057@InterfaceAudience.Private
058public abstract class ClientScanner extends AbstractClientScanner {
059
060  private static final Logger LOG = LoggerFactory.getLogger(ClientScanner.class);
061
062  protected final Scan scan;
063  protected boolean closed = false;
064  // Current region scanner is against. Gets cleared if current region goes
065  // wonky: e.g. if it splits on us.
066  protected HRegionInfo currentRegion = null;
067  protected ScannerCallableWithReplicas callable = null;
068  protected Queue<Result> cache;
069  private final ScanResultCache scanResultCache;
070  protected final int caching;
071  protected long lastNext;
072  // Keep lastResult returned successfully in case we have to reset scanner.
073  protected Result lastResult = null;
074  protected final long maxScannerResultSize;
075  private final ClusterConnection connection;
076  protected final TableName tableName;
077  protected final int scannerTimeout;
078  protected boolean scanMetricsPublished = false;
079  protected RpcRetryingCaller<Result[]> caller;
080  protected RpcControllerFactory rpcControllerFactory;
081  protected Configuration conf;
082  // The timeout on the primary. Applicable if there are multiple replicas for a region
083  // In that case, we will only wait for this much timeout on the primary before going
084  // to the replicas and trying the same scan. Note that the retries will still happen
085  // on each replica and the first successful results will be taken. A timeout of 0 is
086  // disallowed.
087  protected final int primaryOperationTimeout;
088  private int retries;
089  protected final ExecutorService pool;
090
091  /**
092   * Create a new ClientScanner for the specified table Note that the passed {@link Scan}'s start
093   * row maybe changed changed.
094   * @param conf The {@link Configuration} to use.
095   * @param scan {@link Scan} to use in this scanner
096   * @param tableName The table that we wish to scan
097   * @param connection Connection identifying the cluster
098   * @throws IOException
099   */
100  public ClientScanner(final Configuration conf, final Scan scan, final TableName tableName,
101      ClusterConnection connection, RpcRetryingCallerFactory rpcFactory,
102      RpcControllerFactory controllerFactory, ExecutorService pool, int primaryOperationTimeout)
103      throws IOException {
104    if (LOG.isTraceEnabled()) {
105      LOG.trace(
106        "Scan table=" + tableName + ", startRow=" + Bytes.toStringBinary(scan.getStartRow()));
107    }
108    this.scan = scan;
109    this.tableName = tableName;
110    this.lastNext = System.currentTimeMillis();
111    this.connection = connection;
112    this.pool = pool;
113    this.primaryOperationTimeout = primaryOperationTimeout;
114    this.retries = conf.getInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER,
115      HConstants.DEFAULT_HBASE_CLIENT_RETRIES_NUMBER);
116    if (scan.getMaxResultSize() > 0) {
117      this.maxScannerResultSize = scan.getMaxResultSize();
118    } else {
119      this.maxScannerResultSize = conf.getLong(HConstants.HBASE_CLIENT_SCANNER_MAX_RESULT_SIZE_KEY,
120        HConstants.DEFAULT_HBASE_CLIENT_SCANNER_MAX_RESULT_SIZE);
121    }
122    this.scannerTimeout =
123        HBaseConfiguration.getInt(conf, HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD,
124          HConstants.HBASE_REGIONSERVER_LEASE_PERIOD_KEY,
125          HConstants.DEFAULT_HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD);
126
127    // check if application wants to collect scan metrics
128    initScanMetrics(scan);
129
130    // Use the caching from the Scan. If not set, use the default cache setting for this table.
131    if (this.scan.getCaching() > 0) {
132      this.caching = this.scan.getCaching();
133    } else {
134      this.caching = conf.getInt(HConstants.HBASE_CLIENT_SCANNER_CACHING,
135        HConstants.DEFAULT_HBASE_CLIENT_SCANNER_CACHING);
136    }
137
138    this.caller = rpcFactory.<Result[]> newCaller();
139    this.rpcControllerFactory = controllerFactory;
140
141    this.conf = conf;
142
143    this.scanResultCache = createScanResultCache(scan);
144    initCache();
145  }
146
147  protected ClusterConnection getConnection() {
148    return this.connection;
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  @VisibleForTesting
188  protected long getMaxResultSize() {
189    return maxScannerResultSize;
190  }
191
192  private void closeScanner() throws IOException {
193    if (this.callable != null) {
194      this.callable.setClose();
195      call(callable, caller, scannerTimeout, false);
196      this.callable = null;
197    }
198  }
199
200  /**
201   * Will be called in moveToNextRegion when currentRegion is null. Abstract because for normal
202   * scan, we will start next scan from the endKey of the currentRegion, and for reversed scan, we
203   * will start next scan from the startKey of the currentRegion.
204   * @return {@code false} if we have reached the stop row. Otherwise {@code true}.
205   */
206  protected abstract boolean setNewStartKey();
207
208  /**
209   * Will be called in moveToNextRegion to create ScannerCallable. Abstract because for reversed
210   * scan we need to create a ReversedScannerCallable.
211   */
212  protected abstract ScannerCallable createScannerCallable();
213
214  /**
215   * Close the previous scanner and create a new ScannerCallable for the next scanner.
216   * <p>
217   * Marked as protected only because TestClientScanner need to override this method.
218   * @return false if we should terminate the scan. Otherwise
219   */
220  @VisibleForTesting
221  protected boolean moveToNextRegion() {
222    // Close the previous scanner if it's open
223    try {
224      closeScanner();
225    } catch (IOException e) {
226      // not a big deal continue
227      if (LOG.isDebugEnabled()) {
228        LOG.debug("close scanner for " + currentRegion + " failed", e);
229      }
230    }
231    if (currentRegion != null) {
232      if (!setNewStartKey()) {
233        return false;
234      }
235      scan.resetMvccReadPoint();
236      if (LOG.isTraceEnabled()) {
237        LOG.trace("Finished " + this.currentRegion);
238      }
239    }
240    if (LOG.isDebugEnabled() && this.currentRegion != null) {
241      // Only worth logging if NOT first region in scan.
242      LOG.debug(
243        "Advancing internal scanner to startKey at '" + Bytes.toStringBinary(scan.getStartRow()) +
244            "', " + (scan.includeStartRow() ? "inclusive" : "exclusive"));
245    }
246    // clear the current region, we will set a new value to it after the first call of the new
247    // callable.
248    this.currentRegion = null;
249    this.callable =
250        new ScannerCallableWithReplicas(getTable(), getConnection(), createScannerCallable(), pool,
251            primaryOperationTimeout, scan, getRetries(), scannerTimeout, caching, conf, caller);
252    this.callable.setCaching(this.caching);
253    incRegionCountMetrics(scanMetrics);
254    return true;
255  }
256
257  @VisibleForTesting
258  boolean isAnyRPCcancelled() {
259    return callable.isAnyRPCcancelled();
260  }
261
262  private Result[] call(ScannerCallableWithReplicas callable, RpcRetryingCaller<Result[]> caller,
263      int scannerTimeout, boolean updateCurrentRegion) throws IOException {
264    if (Thread.interrupted()) {
265      throw new InterruptedIOException();
266    }
267    // callWithoutRetries is at this layer. Within the ScannerCallableWithReplicas,
268    // we do a callWithRetries
269    Result[] rrs = caller.callWithoutRetries(callable, scannerTimeout);
270    if (currentRegion == null && updateCurrentRegion) {
271      currentRegion = callable.getHRegionInfo();
272    }
273    return rrs;
274  }
275
276  /**
277   * Publish the scan metrics. For now, we use scan.setAttribute to pass the metrics back to the
278   * application or TableInputFormat.Later, we could push it to other systems. We don't use metrics
279   * framework because it doesn't support multi-instances of the same metrics on the same machine;
280   * for scan/map reduce scenarios, we will have multiple scans running at the same time. By
281   * default, scan metrics are disabled; if the application wants to collect them, this behavior can
282   * be turned on by calling calling {@link Scan#setScanMetricsEnabled(boolean)}
283   */
284  protected void writeScanMetrics() {
285    if (this.scanMetrics == null || scanMetricsPublished) {
286      return;
287    }
288    // Publish ScanMetrics to the Scan Object.
289    // As we have claimed in the comment of Scan.getScanMetrics, this relies on that user will not
290    // call ResultScanner.getScanMetrics and reset the ScanMetrics. Otherwise the metrics published
291    // to Scan will be messed up.
292    scan.setAttribute(Scan.SCAN_ATTRIBUTES_METRICS_DATA,
293      ProtobufUtil.toScanMetrics(scanMetrics, false).toByteArray());
294    scanMetricsPublished = true;
295  }
296
297  protected void initSyncCache() {
298    cache = new ArrayDeque<>();
299  }
300
301  protected Result nextWithSyncCache() throws IOException {
302    Result result = cache.poll();
303    if (result != null) {
304      return result;
305    }
306    // If there is nothing left in the cache and the scanner is closed,
307    // return a no-op
308    if (this.closed) {
309      return null;
310    }
311
312    loadCache();
313
314    // try again to load from cache
315    result = cache.poll();
316
317    // if we exhausted this scanner before calling close, write out the scan metrics
318    if (result == null) {
319      writeScanMetrics();
320    }
321    return result;
322  }
323
324  @VisibleForTesting
325  public int getCacheSize() {
326    return cache != null ? cache.size() : 0;
327  }
328
329  private boolean scanExhausted(Result[] values) {
330    return callable.moreResultsForScan() == MoreResults.NO;
331  }
332
333  private boolean regionExhausted(Result[] values) {
334    // 1. Not a heartbeat message and we get nothing, this means the region is exhausted. And in the
335    // old time we always return empty result for a open scanner operation so we add a check here to
336    // keep compatible with the old logic. Should remove the isOpenScanner in the future.
337    // 2. Server tells us that it has no more results for this region.
338    return (values.length == 0 && !callable.isHeartbeatMessage()) ||
339        callable.moreResultsInRegion() == MoreResults.NO;
340  }
341
342  private void closeScannerIfExhausted(boolean exhausted) throws IOException {
343    if (exhausted) {
344      closeScanner();
345    }
346  }
347
348  private void handleScanError(DoNotRetryIOException e,
349      MutableBoolean retryAfterOutOfOrderException, int retriesLeft) throws DoNotRetryIOException {
350    // An exception was thrown which makes any partial results that we were collecting
351    // invalid. The scanner will need to be reset to the beginning of a row.
352    scanResultCache.clear();
353
354    // Unfortunately, DNRIOE is used in two different semantics.
355    // (1) The first is to close the client scanner and bubble up the exception all the way
356    // to the application. This is preferred when the exception is really un-recoverable
357    // (like CorruptHFileException, etc). Plain DoNotRetryIOException also falls into this
358    // bucket usually.
359    // (2) Second semantics is to close the current region scanner only, but continue the
360    // client scanner by overriding the exception. This is usually UnknownScannerException,
361    // OutOfOrderScannerNextException, etc where the region scanner has to be closed, but the
362    // application-level ClientScanner has to continue without bubbling up the exception to
363    // the client. See RSRpcServices to see how it throws DNRIOE's.
364    // See also: HBASE-16604, HBASE-17187
365
366    // If exception is any but the list below throw it back to the client; else setup
367    // the scanner and retry.
368    Throwable cause = e.getCause();
369    if ((cause != null && cause instanceof NotServingRegionException) ||
370        (cause != null && cause instanceof RegionServerStoppedException) ||
371        e instanceof OutOfOrderScannerNextException || e instanceof UnknownScannerException ||
372        e instanceof ScannerResetException) {
373      // Pass. It is easier writing the if loop test as list of what is allowed rather than
374      // as a list of what is not allowed... so if in here, it means we do not throw.
375      if (retriesLeft <= 0) {
376        throw e; // no more retries
377      }
378    } else {
379      throw e;
380    }
381
382    // Else, its signal from depths of ScannerCallable that we need to reset the scanner.
383    if (this.lastResult != null) {
384      // The region has moved. We need to open a brand new scanner at the new location.
385      // Reset the startRow to the row we've seen last so that the new scanner starts at
386      // the correct row. Otherwise we may see previously returned rows again.
387      // If the lastRow is not partial, then we should start from the next row. As now we can
388      // exclude the start row, the logic here is the same for both normal scan and reversed scan.
389      // If lastResult is partial then include it, otherwise exclude it.
390      scan.withStartRow(lastResult.getRow(), lastResult.mayHaveMoreCellsInRow());
391    }
392    if (e instanceof OutOfOrderScannerNextException) {
393      if (retryAfterOutOfOrderException.isTrue()) {
394        retryAfterOutOfOrderException.setValue(false);
395      } else {
396        // TODO: Why wrap this in a DNRIOE when it already is a DNRIOE?
397        throw new DoNotRetryIOException(
398            "Failed after retry of OutOfOrderScannerNextException: was there a rpc timeout?", e);
399      }
400    }
401    // Clear region.
402    this.currentRegion = null;
403    // Set this to zero so we don't try and do an rpc and close on remote server when
404    // the exception we got was UnknownScanner or the Server is going down.
405    callable = null;
406  }
407
408  /**
409   * Contact the servers to load more {@link Result}s in the cache.
410   */
411  protected void loadCache() throws IOException {
412    // check if scanner was closed during previous prefetch
413    if (closed) {
414      return;
415    }
416    long remainingResultSize = maxScannerResultSize;
417    int countdown = this.caching;
418    // This is possible if we just stopped at the boundary of a region in the previous call.
419    if (callable == null && !moveToNextRegion()) {
420      closed = true;
421      return;
422    }
423    // This flag is set when we want to skip the result returned. We do
424    // this when we reset scanner because it split under us.
425    MutableBoolean retryAfterOutOfOrderException = new MutableBoolean(true);
426    // Even if we are retrying due to UnknownScannerException, ScannerResetException, etc. we should
427    // make sure that we are not retrying indefinitely.
428    int retriesLeft = getRetries();
429    for (;;) {
430      Result[] values;
431      try {
432        // Server returns a null values if scanning is to stop. Else,
433        // returns an empty array if scanning is to go on and we've just
434        // exhausted current region.
435        // now we will also fetch data when openScanner, so do not make a next call again if values
436        // is already non-null.
437        values = call(callable, caller, scannerTimeout, true);
438        // When the replica switch happens, we need to do certain operations again.
439        // The callable will openScanner with the right startkey but we need to pick up
440        // from there. Bypass the rest of the loop and let the catch-up happen in the beginning
441        // of the loop as it happens for the cases where we see exceptions.
442        if (callable.switchedToADifferentReplica()) {
443          // Any accumulated partial results are no longer valid since the callable will
444          // openScanner with the correct startkey and we must pick up from there
445          scanResultCache.clear();
446          this.currentRegion = callable.getHRegionInfo();
447        }
448        retryAfterOutOfOrderException.setValue(true);
449      } catch (DoNotRetryIOException e) {
450        handleScanError(e, retryAfterOutOfOrderException, retriesLeft--);
451        // reopen the scanner
452        if (!moveToNextRegion()) {
453          break;
454        }
455        continue;
456      }
457      long currentTime = System.currentTimeMillis();
458      if (this.scanMetrics != null) {
459        this.scanMetrics.sumOfMillisSecBetweenNexts.addAndGet(currentTime - lastNext);
460      }
461      lastNext = currentTime;
462      // Groom the array of Results that we received back from the server before adding that
463      // Results to the scanner's cache. If partial results are not allowed to be seen by the
464      // caller, all book keeping will be performed within this method.
465      int numberOfCompleteRowsBefore = scanResultCache.numberOfCompleteRows();
466      Result[] resultsToAddToCache =
467          scanResultCache.addAndGet(values, callable.isHeartbeatMessage());
468      int numberOfCompleteRows =
469          scanResultCache.numberOfCompleteRows() - numberOfCompleteRowsBefore;
470      for (Result rs : resultsToAddToCache) {
471        cache.add(rs);
472        long estimatedHeapSizeOfResult = calcEstimatedSize(rs);
473        countdown--;
474        remainingResultSize -= estimatedHeapSizeOfResult;
475        addEstimatedSize(estimatedHeapSizeOfResult);
476        this.lastResult = rs;
477      }
478
479      if (scan.getLimit() > 0) {
480        int newLimit = scan.getLimit() - numberOfCompleteRows;
481        assert newLimit >= 0;
482        scan.setLimit(newLimit);
483      }
484      if (scan.getLimit() == 0 || scanExhausted(values)) {
485        closeScanner();
486        closed = true;
487        break;
488      }
489      boolean regionExhausted = regionExhausted(values);
490      if (callable.isHeartbeatMessage()) {
491        if (!cache.isEmpty()) {
492          // Caller of this method just wants a Result. If we see a heartbeat message, it means
493          // processing of the scan is taking a long time server side. Rather than continue to
494          // loop until a limit (e.g. size or caching) is reached, break out early to avoid causing
495          // unnecesary delays to the caller
496          LOG.trace("Heartbeat message received and cache contains Results. " +
497            "Breaking out of scan loop");
498          // we know that the region has not been exhausted yet so just break without calling
499          // closeScannerIfExhausted
500          break;
501        }
502      }
503      if (cache.isEmpty() && !closed && scan.isNeedCursorResult()) {
504        if (callable.isHeartbeatMessage() && callable.getCursor() != null) {
505          // Use cursor row key from server
506          cache.add(Result.createCursorResult(callable.getCursor()));
507          break;
508        }
509        if (values.length > 0) {
510          // It is size limit exceed and we need return the last Result's row.
511          // When user setBatch and the scanner is reopened, the server may return Results that
512          // user has seen and the last Result can not be seen because the number is not enough.
513          // So the row keys of results may not be same, we must use the last one.
514          cache.add(Result.createCursorResult(new Cursor(values[values.length - 1].getRow())));
515          break;
516        }
517      }
518      if (countdown <= 0) {
519        // we have enough result.
520        closeScannerIfExhausted(regionExhausted);
521        break;
522      }
523      if (remainingResultSize <= 0) {
524        if (!cache.isEmpty()) {
525          closeScannerIfExhausted(regionExhausted);
526          break;
527        } else {
528          // we have reached the max result size but we still can not find anything to return to the
529          // user. Reset the maxResultSize and try again.
530          remainingResultSize = maxScannerResultSize;
531        }
532      }
533      // we are done with the current region
534      if (regionExhausted) {
535        if (!moveToNextRegion()) {
536          closed = true;
537          break;
538        }
539      }
540    }
541  }
542
543  protected void addEstimatedSize(long estimatedHeapSizeOfResult) {
544    return;
545  }
546
547  @VisibleForTesting
548  public int getCacheCount() {
549    return cache != null ? cache.size() : 0;
550  }
551
552  @Override
553  public void close() {
554    if (!scanMetricsPublished) writeScanMetrics();
555    if (callable != null) {
556      callable.setClose();
557      try {
558        call(callable, caller, scannerTimeout, false);
559      } catch (UnknownScannerException e) {
560        // We used to catch this error, interpret, and rethrow. However, we
561        // have since decided that it's not nice for a scanner's close to
562        // throw exceptions. Chances are it was just due to lease time out.
563        LOG.debug("scanner failed to close", e);
564      } catch (IOException e) {
565        /* An exception other than UnknownScanner is unexpected. */
566        LOG.warn("scanner failed to close.", e);
567      }
568      callable = null;
569    }
570    closed = true;
571  }
572
573  @Override
574  public boolean renewLease() {
575    if (callable == null) {
576      return false;
577    }
578    // do not return any rows, do not advance the scanner
579    callable.setRenew(true);
580    try {
581      this.caller.callWithoutRetries(callable, this.scannerTimeout);
582      return true;
583    } catch (Exception e) {
584      LOG.debug("scanner failed to renew lease", e);
585      return false;
586    } finally {
587      callable.setRenew(false);
588    }
589  }
590
591  protected void initCache() {
592    initSyncCache();
593  }
594
595  @Override
596  public Result next() throws IOException {
597    return nextWithSyncCache();
598  }
599}