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