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  
19  package org.apache.hadoop.hbase.client;
20  
21  import com.google.protobuf.ServiceException;
22  import com.google.protobuf.TextFormat;
23  
24  import java.io.IOException;
25  import java.io.InterruptedIOException;
26  import java.net.UnknownHostException;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.hadoop.conf.Configuration;
33  import org.apache.hadoop.hbase.Cell;
34  import org.apache.hadoop.hbase.CellUtil;
35  import org.apache.hadoop.hbase.DoNotRetryIOException;
36  import org.apache.hadoop.hbase.HBaseIOException;
37  import org.apache.hadoop.hbase.HRegionInfo;
38  import org.apache.hadoop.hbase.HRegionLocation;
39  import org.apache.hadoop.hbase.NotServingRegionException;
40  import org.apache.hadoop.hbase.RegionLocations;
41  import org.apache.hadoop.hbase.ServerName;
42  import org.apache.hadoop.hbase.TableName;
43  import org.apache.hadoop.hbase.UnknownScannerException;
44  import org.apache.hadoop.hbase.classification.InterfaceAudience;
45  import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
46  import org.apache.hadoop.hbase.exceptions.ScannerResetException;
47  import org.apache.hadoop.hbase.ipc.HBaseRpcController;
48  import org.apache.hadoop.hbase.ipc.RpcControllerFactory;
49  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
50  import org.apache.hadoop.hbase.protobuf.RequestConverter;
51  import org.apache.hadoop.hbase.protobuf.ResponseConverter;
52  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.ScanRequest;
53  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.ScanResponse;
54  import org.apache.hadoop.hbase.regionserver.RegionServerStoppedException;
55  import org.apache.hadoop.net.DNS;
56  
57  /**
58   * Scanner operations such as create, next, etc.
59   * Used by {@link ResultScanner}s made by {@link HTable}. Passed to a retrying caller such as
60   * {@link RpcRetryingCaller} so fails are retried.
61   */
62  @InterfaceAudience.Private
63  public class ScannerCallable extends RegionServerCallable<Result[]> {
64    public static final String LOG_SCANNER_LATENCY_CUTOFF
65      = "hbase.client.log.scanner.latency.cutoff";
66    public static final String LOG_SCANNER_ACTIVITY = "hbase.client.log.scanner.activity";
67  
68    // Keeping LOG public as it is being used in TestScannerHeartbeatMessages
69    public static final Log LOG = LogFactory.getLog(ScannerCallable.class);
70    protected long scannerId = -1L;
71    protected boolean instantiated = false;
72    protected boolean closed = false;
73    protected boolean renew = false;
74    protected final Scan scan;
75    private int caching = 1;
76    protected final ClusterConnection cConnection;
77    protected ScanMetrics scanMetrics;
78    private boolean logScannerActivity = false;
79    private int logCutOffLatency = 1000;
80    private static String myAddress;
81    protected final int id;
82  
83    enum MoreResults {
84      YES, NO, UNKNOWN
85    }
86  
87    private MoreResults moreResultsInRegion;
88    private MoreResults moreResultsForScan;
89  
90    /**
91     * Saves whether or not the most recent response from the server was a heartbeat message.
92     * Heartbeat messages are identified by the flag {@link ScanResponse#getHeartbeatMessage()}
93     */
94    protected boolean heartbeatMessage = false;
95    static {
96      try {
97        myAddress = DNS.getDefaultHost("default", "default");
98      } catch (UnknownHostException uhe) {
99        LOG.error("cannot determine my address", uhe);
100     }
101   }
102 
103   protected Cursor cursor;
104 
105   // indicate if it is a remote server call
106   protected boolean isRegionServerRemote = true;
107   private long nextCallSeq = 0;
108   protected RpcControllerFactory controllerFactory;
109   protected HBaseRpcController controller;
110 
111   /**
112    * @param connection which connection
113    * @param tableName table callable is on
114    * @param scan the scan to execute
115    * @param scanMetrics the ScanMetrics to used, if it is null,
116    *        ScannerCallable won't collect metrics
117    * @param rpcControllerFactory factory to use when creating
118    *        {@link com.google.protobuf.RpcController}
119    */
120   public ScannerCallable (ClusterConnection connection, TableName tableName, Scan scan,
121       ScanMetrics scanMetrics, RpcControllerFactory rpcControllerFactory) {
122     this(connection, tableName, scan, scanMetrics, rpcControllerFactory, 0);
123   }
124   /**
125    *
126    * @param connection
127    * @param tableName
128    * @param scan
129    * @param scanMetrics
130    * @param id the replicaId
131    */
132   public ScannerCallable (ClusterConnection connection, TableName tableName, Scan scan,
133       ScanMetrics scanMetrics, RpcControllerFactory rpcControllerFactory, int id) {
134     super(connection, tableName, scan.getStartRow(), scan.getPriority());
135     this.id = id;
136     this.cConnection = connection;
137     this.scan = scan;
138     this.scanMetrics = scanMetrics;
139     Configuration conf = connection.getConfiguration();
140     logScannerActivity = conf.getBoolean(LOG_SCANNER_ACTIVITY, false);
141     logCutOffLatency = conf.getInt(LOG_SCANNER_LATENCY_CUTOFF, 1000);
142     this.controllerFactory = rpcControllerFactory;
143     this.controller = rpcControllerFactory.newController();
144   }
145 
146   HBaseRpcController getController() {
147     return controller;
148   }
149 
150   /**
151    * @param reload force reload of server location
152    * @throws IOException
153    */
154   @Override
155   public void prepare(boolean reload) throws IOException {
156     if (Thread.interrupted()) {
157       throw new InterruptedIOException();
158     }
159     RegionLocations rl = RpcRetryingCallerWithReadReplicas.getRegionLocations(!reload,
160         id, getConnection(), getTableName(), getRow());
161     location = id < rl.size() ? rl.getRegionLocation(id) : null;
162     if (location == null || location.getServerName() == null) {
163       // With this exception, there will be a retry. The location can be null for a replica
164       //  when the table is created or after a split.
165       throw new HBaseIOException("There is no location for replica id #" + id);
166     }
167     ServerName dest = location.getServerName();
168     setStub(super.getConnection().getClient(dest));
169     if (!instantiated || reload) {
170       checkIfRegionServerIsRemote();
171       instantiated = true;
172     }
173     cursor = null;
174     // check how often we retry.
175     // HConnectionManager will call instantiateServer with reload==true
176     // if and only if for retries.
177     if (reload && this.scanMetrics != null) {
178       this.scanMetrics.countOfRPCRetries.incrementAndGet();
179       if (isRegionServerRemote) {
180         this.scanMetrics.countOfRemoteRPCRetries.incrementAndGet();
181       }
182     }
183   }
184 
185   /**
186    * compare the local machine hostname with region server's hostname
187    * to decide if hbase client connects to a remote region server
188    */
189   protected void checkIfRegionServerIsRemote() {
190     if (getLocation().getHostname().equalsIgnoreCase(myAddress)) {
191       isRegionServerRemote = false;
192     } else {
193       isRegionServerRemote = true;
194     }
195   }
196 
197   private ScanResponse next() throws IOException {
198     // Reset the heartbeat flag prior to each RPC in case an exception is thrown by the server
199     setHeartbeatMessage(false);
200     incRPCcallsMetrics();
201     ScanRequest request = RequestConverter.buildScanRequest(scannerId, caching, false, nextCallSeq,
202       this.scanMetrics != null, renew, scan.getLimit());
203     try {
204       ScanResponse response = getStub().scan(controller, request);
205       nextCallSeq++;
206       return response;
207     } catch (Exception e) {
208       IOException ioe = ProtobufUtil.handleRemoteException(e);
209       if (logScannerActivity) {
210         LOG.info("Got exception making request " + TextFormat.shortDebugString(request) + " to " +
211             getLocation(),
212           e);
213       }
214       if (logScannerActivity) {
215         if (ioe instanceof UnknownScannerException) {
216           try {
217             HRegionLocation location =
218                 getConnection().relocateRegion(getTableName(), scan.getStartRow());
219             LOG.info("Scanner=" + scannerId + " expired, current region location is "
220                 + location.toString());
221           } catch (Throwable t) {
222             LOG.info("Failed to relocate region", t);
223           }
224         } else if (ioe instanceof ScannerResetException) {
225           LOG.info("Scanner=" + scannerId + " has received an exception, and the server "
226               + "asked us to reset the scanner state.",
227             ioe);
228         }
229       }
230       // The below convertion of exceptions into DoNotRetryExceptions is a little strange.
231       // Why not just have these exceptions implment DNRIOE you ask? Well, usually we want
232       // ServerCallable#withRetries to just retry when it gets these exceptions. In here in
233       // a scan when doing a next in particular, we want to break out and get the scanner to
234       // reset itself up again. Throwing a DNRIOE is how we signal this to happen (its ugly,
235       // yeah and hard to follow and in need of a refactor).
236       if (ioe instanceof NotServingRegionException) {
237         // Throw a DNRE so that we break out of cycle of calling NSRE
238         // when what we need is to open scanner against new location.
239         // Attach NSRE to signal client that it needs to re-setup scanner.
240         if (this.scanMetrics != null) {
241           this.scanMetrics.countOfNSRE.incrementAndGet();
242         }
243         throw new DoNotRetryIOException("Resetting the scanner -- see exception cause", ioe);
244       } else if (ioe instanceof RegionServerStoppedException) {
245         // Throw a DNRE so that we break out of cycle of the retries and instead go and
246         // open scanner against new location.
247         throw new DoNotRetryIOException("Resetting the scanner -- see exception cause", ioe);
248       } else {
249         // The outer layers will retry
250         throw ioe;
251       }
252     }
253   }
254 
255   private void setAlreadyClosed() {
256     this.scannerId = -1L;
257     this.closed = true;
258   }
259 
260   @Override
261   public Result[] call(int callTimeout) throws IOException {
262     if (Thread.interrupted()) {
263       throw new InterruptedIOException();
264     }
265     if (closed) {
266       close();
267       return null;
268     }
269     controller.reset();
270     controller.setPriority(getTableName());
271     controller.setCallTimeout(callTimeout);
272     ScanResponse response;
273     if (this.scannerId == -1L) {
274       response = openScanner();
275     } else {
276       response = next();
277     }
278     long timestamp = System.currentTimeMillis();
279     boolean isHeartBeat = response.hasHeartbeatMessage() && response.getHeartbeatMessage();
280     setHeartbeatMessage(isHeartBeat);
281     if (isHeartBeat && scan.isNeedCursorResult() && response.hasCursor()) {
282       cursor = ProtobufUtil.toCursor(response.getCursor());
283     }
284     Result[] rrs = ResponseConverter.getResults(controller.cellScanner(), response);
285     if (logScannerActivity) {
286       long now = System.currentTimeMillis();
287       if (now - timestamp > logCutOffLatency) {
288         int rows = rrs == null ? 0 : rrs.length;
289         LOG.info("Took " + (now - timestamp) + "ms to fetch " + rows + " rows from scanner="
290             + scannerId);
291       }
292     }
293     updateServerSideMetrics(response);
294     // moreResults is only used for the case where a filter exhausts all elements
295     if (response.hasMoreResults()) {
296       if (response.getMoreResults()) {
297         setMoreResultsForScan(MoreResults.YES);
298       } else {
299         setMoreResultsForScan(MoreResults.NO);
300         setAlreadyClosed();
301       }
302     } else {
303       setMoreResultsForScan(MoreResults.UNKNOWN);
304     }
305     if (response.hasMoreResultsInRegion()) {
306       if (response.getMoreResultsInRegion()) {
307         setMoreResultsInRegion(MoreResults.YES);
308       } else {
309         setMoreResultsInRegion(MoreResults.NO);
310         setAlreadyClosed();
311       }
312     } else {
313       setMoreResultsInRegion(MoreResults.UNKNOWN);
314     }
315     updateResultsMetrics(rrs);
316     return rrs;
317   }
318 
319   /**
320    * @return true when the most recent RPC response indicated that the response was a heartbeat
321    *         message. Heartbeat messages are sent back from the server when the processing of the
322    *         scan request exceeds a certain time threshold. Heartbeats allow the server to avoid
323    *         timeouts during long running scan operations.
324    */
325   boolean isHeartbeatMessage() {
326     return heartbeatMessage;
327   }
328 
329   public Cursor getCursor() {
330     return cursor;
331   }
332 
333   private void setHeartbeatMessage(boolean heartbeatMessage) {
334     this.heartbeatMessage = heartbeatMessage;
335   }
336 
337   private void incRPCcallsMetrics() {
338     if (this.scanMetrics == null) {
339       return;
340     }
341     this.scanMetrics.countOfRPCcalls.incrementAndGet();
342     if (isRegionServerRemote) {
343       this.scanMetrics.countOfRemoteRPCcalls.incrementAndGet();
344     }
345   }
346 
347   protected void updateResultsMetrics(Result[] rrs) {
348     if (this.scanMetrics == null || rrs == null || rrs.length == 0) {
349       return;
350     }
351     long resultSize = 0;
352     for (Result rr : rrs) {
353       for (Cell cell : rr.rawCells()) {
354         resultSize += CellUtil.estimatedSerializedSizeOf(cell);
355       }
356     }
357     this.scanMetrics.countOfBytesInResults.addAndGet(resultSize);
358     if (isRegionServerRemote) {
359       this.scanMetrics.countOfBytesInRemoteResults.addAndGet(resultSize);
360     }
361   }
362 
363   /**
364    * Use the scan metrics returned by the server to add to the identically named counters in the
365    * client side metrics. If a counter does not exist with the same name as the server side metric,
366    * the attempt to increase the counter will fail.
367    * @param response
368    */
369   private void updateServerSideMetrics(ScanResponse response) {
370     if (this.scanMetrics == null || response == null || !response.hasScanMetrics()) return;
371 
372     Map<String, Long> serverMetrics = ResponseConverter.getScanMetrics(response);
373     for (Entry<String, Long> entry : serverMetrics.entrySet()) {
374       this.scanMetrics.addToCounter(entry.getKey(), entry.getValue());
375     }
376   }
377 
378   private void close() {
379     if (this.scannerId == -1L) {
380       return;
381     }
382     try {
383       incRPCcallsMetrics();
384       ScanRequest request =
385           RequestConverter.buildScanRequest(this.scannerId, 0, true, this.scanMetrics != null);
386       try {
387         getStub().scan(controller, request);
388       } catch (ServiceException se) {
389         throw ProtobufUtil.getRemoteException(se);
390       }
391     } catch (IOException e) {
392       TableName table = getTableName();
393       String tableDetails = (table == null) ? "" : (" on table: " + table.getNameAsString());
394       LOG.warn("Ignore, probably already closed. Current scan: " + getScan().toString()
395           + tableDetails, e);
396     }
397     this.scannerId = -1L;
398   }
399 
400   private ScanResponse openScanner() throws IOException {
401     incRPCcallsMetrics();
402     ScanRequest request = RequestConverter.buildScanRequest(
403       getLocation().getRegionInfo().getRegionName(), this.scan, this.caching, false);
404     try {
405       ScanResponse response = getStub().scan(controller, request);
406       long id = response.getScannerId();
407       if (logScannerActivity) {
408         LOG.info("Open scanner=" + id + " for scan=" + scan.toString()
409           + " on region " + getLocation().toString());
410       }
411       if (response.hasMvccReadPoint()) {
412         this.scan.setMvccReadPoint(response.getMvccReadPoint());
413       }
414       this.scannerId = id;
415       return response;
416     } catch (Exception e) {
417       throw ProtobufUtil.handleRemoteException(e);
418     }
419   }
420 
421   protected Scan getScan() {
422     return scan;
423   }
424 
425   /**
426    * Call this when the next invocation of call should close the scanner
427    */
428   public void setClose() {
429     this.closed = true;
430   }
431 
432   /**
433    * Indicate whether we make a call only to renew the lease, but without affected the scanner in
434    * any other way.
435    * @param val true if only the lease should be renewed
436    */
437   public void setRenew(boolean val) {
438     this.renew = val;
439   }
440 
441   /**
442    * @return the HRegionInfo for the current region
443    */
444   @Override
445   public HRegionInfo getHRegionInfo() {
446     if (!instantiated) {
447       return null;
448     }
449     return getLocation().getRegionInfo();
450   }
451 
452   /**
453    * Get the number of rows that will be fetched on next
454    * @return the number of rows for caching
455    */
456   public int getCaching() {
457     return caching;
458   }
459 
460   @Override
461   public ClusterConnection getConnection() {
462     return cConnection;
463   }
464 
465   /**
466    * Set the number of rows that will be fetched on next
467    * @param caching the number of rows for caching
468    */
469   public void setCaching(int caching) {
470     this.caching = caching;
471   }
472 
473   public ScannerCallable getScannerCallableForReplica(int id) {
474     ScannerCallable s = new ScannerCallable(this.getConnection(), this.tableName,
475         this.getScan(), this.scanMetrics, controllerFactory, id);
476     s.setCaching(this.caching);
477     return s;
478   }
479 
480   /**
481    * Should the client attempt to fetch more results from this region
482    */
483   MoreResults moreResultsInRegion() {
484     return moreResultsInRegion;
485   }
486 
487   void setMoreResultsInRegion(MoreResults moreResults) {
488     this.moreResultsInRegion = moreResults;
489   }
490 
491   /**
492    * Should the client attempt to fetch more results for the whole scan.
493    */
494   MoreResults moreResultsForScan() {
495     return moreResultsForScan;
496   }
497 
498   void setMoreResultsForScan(MoreResults moreResults) {
499     this.moreResultsForScan = moreResults;
500   }
501 }