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 java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.LinkedList;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.hadoop.conf.Configuration;
27  import org.apache.hadoop.hbase.DoNotRetryIOException;
28  import org.apache.hadoop.hbase.HConstants;
29  import org.apache.hadoop.hbase.HRegionInfo;
30  import org.apache.hadoop.hbase.KeyValue;
31  import org.apache.hadoop.hbase.NotServingRegionException;
32  import org.apache.hadoop.hbase.UnknownScannerException;
33  import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
34  import org.apache.hadoop.hbase.regionserver.RegionServerStoppedException;
35  import org.apache.hadoop.hbase.util.Bytes;
36  import org.apache.hadoop.io.DataOutputBuffer;
37  
38  /**
39   * Implements the scanner interface for the HBase client.
40   * If there are multiple regions in a table, this scanner will iterate
41   * through them all.
42   */
43  public class ClientScanner extends AbstractClientScanner {
44      private final Log LOG = LogFactory.getLog(this.getClass());
45      protected Scan scan;
46      protected boolean closed = false;
47      // Current region scanner is against.  Gets cleared if current region goes
48      // wonky: e.g. if it splits on us.
49      protected HRegionInfo currentRegion = null;
50      private ScannerCallable callable = null;
51      protected final LinkedList<Result> cache = new LinkedList<Result>();
52      protected final int caching;
53      protected long lastNext;
54      // Keep lastResult returned successfully in case we have to reset scanner.
55      protected Result lastResult = null;
56      protected ScanMetrics scanMetrics = null;
57      protected final long maxScannerResultSize;
58      private final HConnection connection;
59      private final byte[] tableName;
60      private final int scannerTimeout;
61  
62      /**
63       * Create a new ClientScanner for the specified table. An HConnection will be
64       * retrieved using the passed Configuration.
65       * Note that the passed {@link Scan}'s start row maybe changed changed. 
66       * 
67       * @param conf The {@link Configuration} to use.
68       * @param scan {@link Scan} to use in this scanner
69       * @param tableName The table that we wish to scan
70       * @throws IOException
71       */
72      public ClientScanner(final Configuration conf, final Scan scan,
73          final byte[] tableName) throws IOException {
74        this(conf, scan, tableName, HConnectionManager.getConnection(conf));
75      }
76   
77      /**
78       * Create a new ClientScanner for the specified table
79       * Note that the passed {@link Scan}'s start row maybe changed changed. 
80       * 
81       * @param conf The {@link Configuration} to use.
82       * @param scan {@link Scan} to use in this scanner
83       * @param tableName The table that we wish to scan
84       * @param connection Connection identifying the cluster
85       * @throws IOException
86       */
87      public ClientScanner(final Configuration conf, final Scan scan,
88        final byte[] tableName, HConnection connection) throws IOException {
89        if (LOG.isDebugEnabled()) {
90          LOG.debug("Creating scanner over "
91              + Bytes.toString(tableName)
92              + " starting at key '" + Bytes.toStringBinary(scan.getStartRow()) + "'");
93        }
94        this.scan = scan;
95        this.tableName = tableName;
96        this.lastNext = System.currentTimeMillis();
97        this.connection = connection;
98        this.maxScannerResultSize = conf.getLong(
99            HConstants.HBASE_CLIENT_SCANNER_MAX_RESULT_SIZE_KEY,
100           HConstants.DEFAULT_HBASE_CLIENT_SCANNER_MAX_RESULT_SIZE);
101       this.scannerTimeout = (int) conf.getLong(
102           HConstants.HBASE_REGIONSERVER_LEASE_PERIOD_KEY,
103           HConstants.DEFAULT_HBASE_REGIONSERVER_LEASE_PERIOD);
104 
105       // check if application wants to collect scan metrics
106       byte[] enableMetrics = scan.getAttribute(
107         Scan.SCAN_ATTRIBUTES_METRICS_ENABLE);
108       if (enableMetrics != null && Bytes.toBoolean(enableMetrics)) {
109         scanMetrics = new ScanMetrics();
110       }
111 
112       // Use the caching from the Scan.  If not set, use the default cache setting for this table.
113       if (this.scan.getCaching() > 0) {
114         this.caching = this.scan.getCaching();
115       } else {
116         this.caching = conf.getInt("hbase.client.scanner.caching", 1);
117       }
118 
119       // initialize the scanner
120       initializeScannerInConstruction();
121     }
122     
123     protected void initializeScannerInConstruction() throws IOException{
124       // initialize the scanner
125       nextScanner(this.caching, false);
126     }
127 
128     protected HConnection getConnection() {
129       return this.connection;
130     }
131 
132     protected byte[] getTableName() {
133       return this.tableName;
134     }
135 
136     protected Scan getScan() {
137       return scan;
138     }
139 
140     protected long getTimestamp() {
141       return lastNext;
142     }
143 
144     // returns true if the passed region endKey
145     protected boolean checkScanStopRow(final byte [] endKey) {
146       if (this.scan.getStopRow().length > 0) {
147         // there is a stop row, check to see if we are past it.
148         byte [] stopRow = scan.getStopRow();
149         int cmp = Bytes.compareTo(stopRow, 0, stopRow.length,
150           endKey, 0, endKey.length);
151         if (cmp <= 0) {
152           // stopRow <= endKey (endKey is equals to or larger than stopRow)
153           // This is a stop.
154           return true;
155         }
156       }
157       return false; //unlikely.
158     }
159 
160     /*
161      * Gets a scanner for the next region.  If this.currentRegion != null, then
162      * we will move to the endrow of this.currentRegion.  Else we will get
163      * scanner at the scan.getStartRow().  We will go no further, just tidy
164      * up outstanding scanners, if <code>currentRegion != null</code> and
165      * <code>done</code> is true.
166      * @param nbRows
167      * @param done Server-side says we're done scanning.
168      */
169     private boolean nextScanner(int nbRows, final boolean done)
170     throws IOException {
171       // Close the previous scanner if it's open
172       if (this.callable != null) {
173         this.callable.setClose();
174         callable.withRetries();
175         this.callable = null;
176       }
177 
178       // Where to start the next scanner
179       byte [] localStartKey;
180 
181       // if we're at end of table, close and return false to stop iterating
182       if (this.currentRegion != null) {
183         byte [] endKey = this.currentRegion.getEndKey();
184         if (endKey == null ||
185             Bytes.equals(endKey, HConstants.EMPTY_BYTE_ARRAY) ||
186             checkScanStopRow(endKey) ||
187             done) {
188           close();
189           if (LOG.isDebugEnabled()) {
190             LOG.debug("Finished with scanning at " + this.currentRegion);
191           }
192           return false;
193         }
194         localStartKey = endKey;
195         if (LOG.isDebugEnabled()) {
196           LOG.debug("Finished with region " + this.currentRegion);
197         }
198       } else {
199         localStartKey = this.scan.getStartRow();
200       }
201 
202       if (LOG.isDebugEnabled()) {
203         LOG.debug("Advancing internal scanner to startKey at '" +
204           Bytes.toStringBinary(localStartKey) + "'");
205       }
206       try {
207         callable = getScannerCallable(localStartKey, nbRows);
208         // Open a scanner on the region server starting at the
209         // beginning of the region
210         callable.withRetries();
211         this.currentRegion = callable.getHRegionInfo();
212         if (this.scanMetrics != null) {
213           this.scanMetrics.countOfRegions.inc();
214         }
215       } catch (IOException e) {
216         close();
217         throw e;
218       }
219       return true;
220     }
221 
222     protected ScannerCallable getScannerCallable(byte [] localStartKey,
223         int nbRows) {
224       scan.setStartRow(localStartKey);
225       ScannerCallable s = new ScannerCallable(getConnection(),
226         getTableName(), scan, this.scanMetrics);
227       s.setCaching(nbRows);
228       return s;
229     }
230 
231     /**
232      * Publish the scan metrics. For now, we use scan.setAttribute to pass the metrics back to the
233      * application or TableInputFormat.Later, we could push it to other systems. We don't use metrics
234      * framework because it doesn't support multi-instances of the same metrics on the same machine;
235      * for scan/map reduce scenarios, we will have multiple scans running at the same time.
236      *
237      * By default, scan metrics are disabled; if the application wants to collect them, this behavior
238      * can be turned on by calling calling:
239      *
240      * scan.setAttribute(SCAN_ATTRIBUTES_METRICS_ENABLE, Bytes.toBytes(Boolean.TRUE))
241      */
242     protected void writeScanMetrics() throws IOException {
243       if (this.scanMetrics == null) {
244         return;
245       }
246       final DataOutputBuffer d = new DataOutputBuffer();
247       scanMetrics.write(d);
248       scan.setAttribute(Scan.SCAN_ATTRIBUTES_METRICS_DATA, d.getData());
249     }
250 
251     public Result next() throws IOException {
252       // If the scanner is closed and there's nothing left in the cache, next is a no-op.
253       if (cache.size() == 0 && this.closed) {
254         return null;
255       }
256       if (cache.size() == 0) {
257         Result [] values = null;
258         long remainingResultSize = maxScannerResultSize;
259         int countdown = this.caching;
260         // We need to reset it if it's a new callable that was created
261         // with a countdown in nextScanner
262         callable.setCaching(this.caching);
263         // This flag is set when we want to skip the result returned.  We do
264         // this when we reset scanner because it split under us.
265         boolean skipFirst = false;
266         do {
267           try {
268             if (skipFirst) {
269               // Skip only the first row (which was the last row of the last
270               // already-processed batch).
271               callable.setCaching(1);
272               values = callable.withRetries();
273               callable.setCaching(this.caching);
274               skipFirst = false;
275             }
276             // Server returns a null values if scanning is to stop.  Else,
277             // returns an empty array if scanning is to go on and we've just
278             // exhausted current region.
279             values = callable.withRetries();
280           } catch (DoNotRetryIOException e) {
281             if (e instanceof UnknownScannerException) {
282               long timeout = lastNext + scannerTimeout;
283               // If we are over the timeout, throw this exception to the client
284               // Else, it's because the region moved and we used the old id
285               // against the new region server; reset the scanner.
286               if (timeout < System.currentTimeMillis()) {
287                 long elapsed = System.currentTimeMillis() - lastNext;
288                 ScannerTimeoutException ex = new ScannerTimeoutException(
289                     elapsed + "ms passed since the last invocation, " +
290                         "timeout is currently set to " + scannerTimeout);
291                 ex.initCause(e);
292                 throw ex;
293               }
294             } else {
295               Throwable cause = e.getCause();
296               if (cause == null || (!(cause instanceof NotServingRegionException)
297                   && !(cause instanceof RegionServerStoppedException))) {
298                 throw e;
299               }
300             }
301             // Else, its signal from depths of ScannerCallable that we got an
302             // NSRE on a next and that we need to reset the scanner.
303             if (this.lastResult != null) {
304               this.scan.setStartRow(this.lastResult.getRow());
305               // Skip first row returned.  We already let it out on previous
306               // invocation.
307               skipFirst = true;
308             }
309             // Clear region
310             this.currentRegion = null;
311             continue;
312           }
313           long currentTime = System.currentTimeMillis();
314           if (this.scanMetrics != null ) {
315             this.scanMetrics.sumOfMillisSecBetweenNexts.inc(currentTime-lastNext);
316           }
317           lastNext = currentTime;
318           if (values != null && values.length > 0) {
319             for (Result rs : values) {
320               cache.add(rs);
321               for (KeyValue kv : rs.raw()) {
322                   remainingResultSize -= kv.heapSize();
323               }
324               countdown--;
325               this.lastResult = rs;
326             }
327           }
328           // Values == null means server-side filter has determined we must STOP
329         } while (remainingResultSize > 0 && countdown > 0 && nextScanner(countdown, values == null));
330       }
331 
332       if (cache.size() > 0) {
333         return cache.poll();
334       }
335 
336       // if we exhausted this scanner before calling close, write out the scan metrics
337       writeScanMetrics();
338       return null;
339     }
340 
341     /**
342      * Get <param>nbRows</param> rows.
343      * How many RPCs are made is determined by the {@link Scan#setCaching(int)}
344      * setting (or hbase.client.scanner.caching in hbase-site.xml).
345      * @param nbRows number of rows to return
346      * @return Between zero and <param>nbRows</param> RowResults.  Scan is done
347      * if returned array is of zero-length (We never return null).
348      * @throws IOException
349      */
350     public Result [] next(int nbRows) throws IOException {
351       // Collect values to be returned here
352       ArrayList<Result> resultSets = new ArrayList<Result>(nbRows);
353       for(int i = 0; i < nbRows; i++) {
354         Result next = next();
355         if (next != null) {
356           resultSets.add(next);
357         } else {
358           break;
359         }
360       }
361       return resultSets.toArray(new Result[resultSets.size()]);
362     }
363 
364     public void close() {
365       if (callable != null) {
366         callable.setClose();
367         try {
368           callable.withRetries();
369         } catch (IOException e) {
370           // We used to catch this error, interpret, and rethrow. However, we
371           // have since decided that it's not nice for a scanner's close to
372           // throw exceptions. Chances are it was just an UnknownScanner
373           // exception due to lease time out.
374         } finally {
375           // we want to output the scan metrics even if an error occurred on close
376           try {
377             writeScanMetrics();
378           } catch (IOException e) {
379             // As above, we still don't want the scanner close() method to throw.
380           }
381         }
382         callable = null;
383       }
384       closed = true;
385     }
386 }