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.common.annotations.VisibleForTesting;
22  
23  import java.io.IOException;
24  import java.io.InterruptedIOException;
25  import java.util.HashSet;
26  import java.util.Set;
27  import java.util.concurrent.Callable;
28  import java.util.concurrent.CancellationException;
29  import java.util.concurrent.ExecutionException;
30  import java.util.concurrent.ExecutorService;
31  import java.util.concurrent.Future;
32  import java.util.concurrent.TimeUnit;
33  import java.util.concurrent.atomic.AtomicBoolean;
34  
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.apache.hadoop.conf.Configuration;
38  import org.apache.hadoop.hbase.DoNotRetryIOException;
39  import org.apache.hadoop.hbase.HRegionInfo;
40  import org.apache.hadoop.hbase.RegionLocations;
41  import org.apache.hadoop.hbase.TableName;
42  import org.apache.hadoop.hbase.classification.InterfaceAudience;
43  import org.apache.hadoop.hbase.client.ScannerCallable.MoreResults;
44  import org.apache.hadoop.hbase.util.Pair;
45  
46  /**
47   * This class has the logic for handling scanners for regions with and without replicas.
48   * 1. A scan is attempted on the default (primary) region
49   * 2. The scanner sends all the RPCs to the default region until it is done, or, there
50   * is a timeout on the default (a timeout of zero is disallowed).
51   * 3. If there is a timeout in (2) above, scanner(s) is opened on the non-default replica(s)
52   * 4. The results from the first successful scanner are taken, and it is stored which server
53   * returned the results.
54   * 5. The next RPCs are done on the above stored server until it is done or there is a timeout,
55   * in which case, the other replicas are queried (as in (3) above).
56   *
57   */
58  @InterfaceAudience.Private
59  class ScannerCallableWithReplicas implements RetryingCallable<Result[]> {
60    private static final Log LOG = LogFactory.getLog(ScannerCallableWithReplicas.class);
61    volatile ScannerCallable currentScannerCallable;
62    AtomicBoolean replicaSwitched = new AtomicBoolean(false);
63    final ClusterConnection cConnection;
64    protected final ExecutorService pool;
65    protected final int timeBeforeReplicas;
66    private final Scan scan;
67    private final int retries;
68    private Result lastResult;
69    private final RpcRetryingCaller<Result[]> caller;
70    private final TableName tableName;
71    private Configuration conf;
72    private int scannerTimeout;
73    private Set<ScannerCallable> outstandingCallables = new HashSet<ScannerCallable>();
74    private boolean someRPCcancelled = false; //required for testing purposes only
75    private int regionReplication = 0;
76  
77    public ScannerCallableWithReplicas(TableName tableName, ClusterConnection cConnection,
78        ScannerCallable baseCallable, ExecutorService pool, int timeBeforeReplicas, Scan scan,
79        int retries, int scannerTimeout, int caching, Configuration conf,
80        RpcRetryingCaller<Result []> caller) {
81      this.currentScannerCallable = baseCallable;
82      this.cConnection = cConnection;
83      this.pool = pool;
84      if (timeBeforeReplicas < 0) {
85        throw new IllegalArgumentException("Invalid value of operation timeout on the primary");
86      }
87      this.timeBeforeReplicas = timeBeforeReplicas;
88      this.scan = scan;
89      this.retries = retries;
90      this.tableName = tableName;
91      this.conf = conf;
92      this.scannerTimeout = scannerTimeout;
93      this.caller = caller;
94    }
95  
96    public void setClose() {
97      currentScannerCallable.setClose();
98    }
99  
100   public void setRenew(boolean val) {
101     currentScannerCallable.setRenew(val);
102   }
103 
104   public void setCaching(int caching) {
105     currentScannerCallable.setCaching(caching);
106   }
107 
108   public int getCaching() {
109     return currentScannerCallable.getCaching();
110   }
111 
112   public HRegionInfo getHRegionInfo() {
113     return currentScannerCallable.getHRegionInfo();
114   }
115 
116   public MoreResults moreResultsInRegion() {
117     return currentScannerCallable.moreResultsInRegion();
118   }
119 
120   public MoreResults moreResultsForScan() {
121     return currentScannerCallable.moreResultsForScan();
122   }
123 
124   @Override
125   public Result [] call(int timeout) throws IOException {
126     // If the active replica callable was closed somewhere, invoke the RPC to
127     // really close it. In the case of regular scanners, this applies. We make couple
128     // of RPCs to a RegionServer, and when that region is exhausted, we set
129     // the closed flag. Then an RPC is required to actually close the scanner.
130     if (currentScannerCallable != null && currentScannerCallable.closed) {
131       // For closing we target that exact scanner (and not do replica fallback like in
132       // the case of normal reads)
133       if (LOG.isTraceEnabled()) {
134         LOG.trace("Closing scanner id=" + currentScannerCallable.scannerId);
135       }
136       Result[] r = currentScannerCallable.call(timeout);
137       currentScannerCallable = null;
138       return r;
139     }
140     // We need to do the following:
141     //1. When a scan goes out to a certain replica (default or not), we need to
142     //   continue to hit that until there is a failure. So store the last successfully invoked
143     //   replica
144     //2. We should close the "losing" scanners (scanners other than the ones we hear back
145     //   from first)
146     //
147     // Since RegionReplication is a table attribute, it wont change as long as table is enabled,
148     // it just needs to be set once.
149 
150     if (regionReplication <= 0) {
151       RegionLocations rl = null;
152       try {
153         rl = RpcRetryingCallerWithReadReplicas.getRegionLocations(true,
154             RegionReplicaUtil.DEFAULT_REPLICA_ID, cConnection, tableName,
155             currentScannerCallable.getRow());
156       } catch (RetriesExhaustedException | DoNotRetryIOException e) {
157         // We cannot get the primary replica region location, it is possible that the region server
158         // hosting meta table is down, it needs to proceed to try cached replicas directly.
159         if (cConnection instanceof ConnectionManager.HConnectionImplementation) {
160           rl = ((ConnectionManager.HConnectionImplementation) cConnection)
161               .getCachedLocation(tableName, currentScannerCallable.getRow());
162           if (rl == null) {
163             throw e;
164           }
165         } else {
166           // For completeness
167           throw e;
168         }
169       }
170       regionReplication = rl.size();
171     }
172     // allocate a boundedcompletion pool of some multiple of number of replicas.
173     // We want to accomodate some RPCs for redundant replica scans (but are still in progress)
174     ResultBoundedCompletionService<Pair<Result[], ScannerCallable>> cs =
175         new ResultBoundedCompletionService<Pair<Result[], ScannerCallable>>(
176             RpcRetryingCallerFactory.instantiate(ScannerCallableWithReplicas.this.conf), pool,
177             regionReplication * 5);
178 
179     AtomicBoolean done = new AtomicBoolean(false);
180     replicaSwitched.set(false);
181     // submit call for the primary replica.
182     addCallsForCurrentReplica(cs);
183     int startIndex = 0;
184 
185     try {
186       // wait for the timeout to see whether the primary responds back
187       Future<Pair<Result[], ScannerCallable>> f = cs.poll(timeBeforeReplicas,
188           TimeUnit.MICROSECONDS); // Yes, microseconds
189       if (f != null) {
190         // After poll, if f is not null, there must be a completed task
191         Pair<Result[], ScannerCallable> r = f.get();
192         if (r != null && r.getSecond() != null) {
193           updateCurrentlyServingReplica(r.getSecond(), r.getFirst(), done, pool);
194         }
195         return r == null ? null : r.getFirst(); //great we got a response
196       }
197     } catch (ExecutionException e) {
198       // We ignore the ExecutionException and continue with the replicas
199       if (LOG.isDebugEnabled()) {
200         LOG.debug("Scan with primary region returns " + e.getCause());
201       }
202 
203       // If rl's size is 1 or scan's consitency is strong, it needs to throw
204       // out the exception from the primary replica
205       if ((regionReplication == 1) || (scan.getConsistency() == Consistency.STRONG)) {
206         // Rethrow the first exception
207         RpcRetryingCallerWithReadReplicas.throwEnrichedException(e, retries);
208       }
209 
210       startIndex = 1;
211     } catch (CancellationException e) {
212       throw new InterruptedIOException(e.getMessage());
213     } catch (InterruptedException e) {
214       throw new InterruptedIOException(e.getMessage());
215     }
216 
217     // submit call for the all of the secondaries at once
218     int endIndex = regionReplication;
219     if (scan.getConsistency() == Consistency.STRONG) {
220       // When scan's consistency is strong, do not send to the secondaries
221       endIndex = 1;
222     } else {
223       // TODO: this may be an overkill for large region replication
224       addCallsForOtherReplicas(cs, 0, regionReplication - 1);
225     }
226 
227     try {
228       Future<Pair<Result[], ScannerCallable>> f = cs.pollForFirstSuccessfullyCompletedTask(timeout,
229           TimeUnit.MILLISECONDS, startIndex, endIndex);
230 
231       if (f == null) {
232         throw new IOException("Failed to get result within timeout, timeout=" + timeout + "ms");
233       }
234       Pair<Result[], ScannerCallable> r = f.get();
235 
236       if (r != null && r.getSecond() != null) {
237         updateCurrentlyServingReplica(r.getSecond(), r.getFirst(), done, pool);
238       }
239       return r == null ? null : r.getFirst(); // great we got an answer
240 
241     } catch (ExecutionException e) {
242       RpcRetryingCallerWithReadReplicas.throwEnrichedException(e, retries);
243     } catch (CancellationException e) {
244       throw new InterruptedIOException(e.getMessage());
245     } catch (InterruptedException e) {
246       throw new InterruptedIOException(e.getMessage());
247     } finally {
248       // We get there because we were interrupted or because one or more of the
249       // calls succeeded or failed. In all case, we stop all our tasks.
250       cs.cancelAll();
251     }
252     LOG.error("Imposible? Arrive at an unreachable line..."); // unreachable
253     throw new IOException("Imposible? Arrive at an unreachable line...");
254   }
255 
256   private void updateCurrentlyServingReplica(ScannerCallable scanner, Result[] result,
257       AtomicBoolean done, ExecutorService pool) {
258     if (done.compareAndSet(false, true)) {
259       if (currentScannerCallable != scanner) replicaSwitched.set(true);
260       currentScannerCallable = scanner;
261       // store where to start the replica scanner from if we need to.
262       if (result != null && result.length != 0) this.lastResult = result[result.length - 1];
263       if (LOG.isTraceEnabled()) {
264         LOG.trace("Setting current scanner as id=" + currentScannerCallable.scannerId +
265             " associated with replica=" + currentScannerCallable.getHRegionInfo().getReplicaId());
266       }
267       // close all outstanding replica scanners but the one we heard back from
268       outstandingCallables.remove(scanner);
269       for (ScannerCallable s : outstandingCallables) {
270         if (LOG.isTraceEnabled()) {
271           LOG.trace("Closing scanner id=" + s.scannerId +
272             ", replica=" + s.getHRegionInfo().getRegionId() +
273             " because slow and replica=" +
274             this.currentScannerCallable.getHRegionInfo().getReplicaId() + " succeeded");
275         }
276         // Submit the "close" to the pool since this might take time, and we don't
277         // want to wait for the "close" to happen yet. The "wait" will happen when
278         // the table is closed (when the awaitTermination of the underlying pool is called)
279         s.setClose();
280         final RetryingRPC r = new RetryingRPC(s);
281         pool.submit(new Callable<Void>(){
282           @Override
283           public Void call() throws Exception {
284             r.call(scannerTimeout);
285             return null;
286           }
287         });
288       }
289       // now clear outstandingCallables since we scheduled a close for all the contained scanners
290       outstandingCallables.clear();
291     }
292   }
293 
294   /**
295    * When a scanner switches in the middle of scanning (the 'next' call fails
296    * for example), the upper layer {@link ClientScanner} needs to know
297    * @return
298    */
299   public boolean switchedToADifferentReplica() {
300     return replicaSwitched.get();
301   }
302 
303   /**
304    * @return true when the most recent RPC response indicated that the response was a heartbeat
305    *         message. Heartbeat messages are sent back from the server when the processing of the
306    *         scan request exceeds a certain time threshold. Heartbeats allow the server to avoid
307    *         timeouts during long running scan operations.
308    */
309   public boolean isHeartbeatMessage() {
310     return currentScannerCallable != null && currentScannerCallable.isHeartbeatMessage();
311   }
312 
313   public Cursor getCursor() {
314     return currentScannerCallable != null ? currentScannerCallable.getCursor() : null;
315   }
316 
317   private void addCallsForCurrentReplica(
318       ResultBoundedCompletionService<Pair<Result[], ScannerCallable>> cs) {
319     RetryingRPC retryingOnReplica = new RetryingRPC(currentScannerCallable);
320     outstandingCallables.add(currentScannerCallable);
321     cs.submit(retryingOnReplica, scannerTimeout, currentScannerCallable.id);
322   }
323 
324   private void addCallsForOtherReplicas(
325       ResultBoundedCompletionService<Pair<Result[], ScannerCallable>> cs, int min, int max) {
326 
327     for (int id = min; id <= max; id++) {
328       if (currentScannerCallable.id == id) {
329         continue; //this was already scheduled earlier
330       }
331       ScannerCallable s = currentScannerCallable.getScannerCallableForReplica(id);
332       setStartRowForReplicaCallable(s);
333       outstandingCallables.add(s);
334       RetryingRPC retryingOnReplica = new RetryingRPC(s);
335       cs.submit(retryingOnReplica, scannerTimeout, id);
336     }
337   }
338 
339   /**
340    * Set the start row for the replica callable based on the state of the last result received.
341    * @param callable The callable to set the start row on
342    */
343   private void setStartRowForReplicaCallable(ScannerCallable callable) {
344     if (this.lastResult == null || callable == null) {
345       return;
346     }
347     // 1. The last result was a partial result which means we have not received all of the cells
348     // for this row. Thus, use the last result's row as the start row. If a replica switch
349     // occurs, the scanner will ensure that any accumulated partial results are cleared,
350     // and the scan can resume from this row.
351     // 2. The last result was not a partial result which means it contained all of the cells for
352     // that row (we no longer need any information from it). Set the start row to the next
353     // closest row that could be seen.
354     callable.getScan().withStartRow(this.lastResult.getRow(), this.lastResult.mayHaveMoreCellsInRow());
355   }
356 
357   @VisibleForTesting
358   boolean isAnyRPCcancelled() {
359     return someRPCcancelled;
360   }
361 
362   class RetryingRPC implements RetryingCallable<Pair<Result[], ScannerCallable>>, Cancellable {
363     final ScannerCallable callable;
364     RpcRetryingCaller<Result[]> caller;
365     private volatile boolean cancelled = false;
366 
367     RetryingRPC(ScannerCallable callable) {
368       this.callable = callable;
369       // For the Consistency.STRONG (default case), we reuse the caller
370       // to keep compatibility with what is done in the past
371       // For the Consistency.TIMELINE case, we can't reuse the caller
372       // since we could be making parallel RPCs (caller.callWithRetries is synchronized
373       // and we can't invoke it multiple times at the same time)
374       this.caller = ScannerCallableWithReplicas.this.caller;
375       if (scan.getConsistency() == Consistency.TIMELINE) {
376         this.caller = RpcRetryingCallerFactory.instantiate(ScannerCallableWithReplicas.this.conf)
377             .<Result[]>newCaller();
378       }
379     }
380 
381     @Override
382     public Pair<Result[], ScannerCallable> call(int callTimeout) throws IOException {
383       // since the retries is done within the ResultBoundedCompletionService,
384       // we don't invoke callWithRetries here
385       if (cancelled) {
386         return null;
387       }
388       Result[] res = this.caller.callWithoutRetries(this.callable, callTimeout);
389       return new Pair<Result[], ScannerCallable>(res, this.callable);
390     }
391 
392     @Override
393     public void prepare(boolean reload) throws IOException {
394       if (cancelled) return;
395 
396       if (Thread.interrupted()) {
397         throw new InterruptedIOException();
398       }
399 
400       callable.prepare(reload);
401     }
402 
403     @Override
404     public void throwable(Throwable t, boolean retrying) {
405       callable.throwable(t, retrying);
406     }
407 
408     @Override
409     public String getExceptionMessageAdditionalDetail() {
410       return callable.getExceptionMessageAdditionalDetail();
411     }
412 
413     @Override
414     public long sleep(long pause, int tries) {
415       return callable.sleep(pause, tries);
416     }
417 
418     @Override
419     public void cancel() {
420       cancelled = true;
421       caller.cancel();
422       if (callable.getController() != null) {
423         callable.getController().startCancel();
424       }
425       someRPCcancelled = true;
426     }
427 
428     @Override
429     public boolean isCancelled() {
430       return cancelled;
431     }
432   }
433 
434   @Override
435   public void prepare(boolean reload) throws IOException {
436   }
437 
438   @Override
439   public void throwable(Throwable t, boolean retrying) {
440     currentScannerCallable.throwable(t, retrying);
441   }
442 
443   @Override
444   public String getExceptionMessageAdditionalDetail() {
445     return currentScannerCallable.getExceptionMessageAdditionalDetail();
446   }
447 
448   @Override
449   public long sleep(long pause, int tries) {
450     return currentScannerCallable.sleep(pause, tries);
451   }
452 }