View Javadoc

1   /**
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  
21  package org.apache.hadoop.hbase.client;
22  
23  
24  import com.google.protobuf.ServiceException;
25  
26  import java.io.IOException;
27  import java.io.InterruptedIOException;
28  import java.util.Collections;
29  import java.util.List;
30  import java.util.concurrent.CancellationException;
31  import java.util.concurrent.ExecutionException;
32  import java.util.concurrent.ExecutorService;
33  import java.util.concurrent.Future;
34  import java.util.concurrent.TimeUnit;
35  
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  import org.apache.hadoop.conf.Configuration;
39  import org.apache.hadoop.hbase.DoNotRetryIOException;
40  import org.apache.hadoop.hbase.HBaseIOException;
41  import org.apache.hadoop.hbase.HConstants;
42  import org.apache.hadoop.hbase.HRegionLocation;
43  import org.apache.hadoop.hbase.RegionLocations;
44  import org.apache.hadoop.hbase.ServerName;
45  import org.apache.hadoop.hbase.TableName;
46  import org.apache.hadoop.hbase.classification.InterfaceAudience;
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.generated.ClientProtos;
52  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
53  
54  /**
55   * Caller that goes to replica if the primary region does no answer within a configurable
56   * timeout. If the timeout is reached, it calls all the secondary replicas, and returns
57   * the first answer. If the answer comes from one of the secondary replica, it will
58   * be marked as stale.
59   */
60  @InterfaceAudience.Private
61  public class RpcRetryingCallerWithReadReplicas {
62    private static final Log LOG = LogFactory.getLog(RpcRetryingCallerWithReadReplicas.class);
63  
64    protected final ExecutorService pool;
65    protected final ClusterConnection cConnection;
66    protected final Configuration conf;
67    protected final Get get;
68    protected final TableName tableName;
69    protected final int timeBeforeReplicas;
70    private final int callTimeout;
71    private final int retries;
72    private final RpcControllerFactory rpcControllerFactory;
73    private final RpcRetryingCallerFactory rpcRetryingCallerFactory;
74  
75    public RpcRetryingCallerWithReadReplicas(
76        RpcControllerFactory rpcControllerFactory, TableName tableName,
77        ClusterConnection cConnection, final Get get,
78        ExecutorService pool, int retries, int callTimeout,
79        int timeBeforeReplicas) {
80      this.rpcControllerFactory = rpcControllerFactory;
81      this.tableName = tableName;
82      this.cConnection = cConnection;
83      this.conf = cConnection.getConfiguration();
84      this.get = get;
85      this.pool = pool;
86      this.retries = retries;
87      this.callTimeout = callTimeout;
88      this.timeBeforeReplicas = timeBeforeReplicas;
89      this.rpcRetryingCallerFactory = new RpcRetryingCallerFactory(conf);
90    }
91  
92    /**
93     * A RegionServerCallable that takes into account the replicas, i.e.
94     * - the call can be on any replica
95     * - we need to stop retrying when the call is completed
96     * - we can be interrupted
97     */
98    class ReplicaRegionServerCallable extends RegionServerCallable<Result> implements Cancellable {
99      final int id;
100     private final HBaseRpcController controller;
101 
102     public ReplicaRegionServerCallable(int id, HRegionLocation location) {
103       super(RpcRetryingCallerWithReadReplicas.this.cConnection,
104           RpcRetryingCallerWithReadReplicas.this.tableName, get.getRow(), HConstants.PRIORITY_UNSET);
105       this.id = id;
106       this.location = location;
107       this.controller = rpcControllerFactory.newController();
108       controller.setPriority(tableName);
109     }
110 
111     @Override
112     public void cancel() {
113       controller.startCancel();
114     }
115 
116     /**
117      * Two responsibilities
118      * - if the call is already completed (by another replica) stops the retries.
119      * - set the location to the right region, depending on the replica.
120      */
121     @Override
122     public void prepare(final boolean reload) throws IOException {
123       if (controller.isCanceled()) return;
124 
125       if (Thread.interrupted()) {
126         throw new InterruptedIOException();
127       }
128 
129       if (reload || location == null) {
130         RegionLocations rl = getRegionLocations(false, id, cConnection, tableName, get.getRow());
131         location = id < rl.size() ? rl.getRegionLocation(id) : null;
132       }
133 
134       if (location == null || location.getServerName() == null) {
135         // With this exception, there will be a retry. The location can be null for a replica
136         //  when the table is created or after a split.
137         throw new HBaseIOException("There is no location for replica id #" + id);
138       }
139 
140       ServerName dest = location.getServerName();
141 
142       setStub(cConnection.getClient(dest));
143     }
144 
145     @Override
146     public Result call(int callTimeout) throws Exception {
147       if (controller.isCanceled()) return null;
148 
149       if (Thread.interrupted()) {
150         throw new InterruptedIOException();
151       }
152 
153       byte[] reg = location.getRegionInfo().getRegionName();
154 
155       ClientProtos.GetRequest request =
156           RequestConverter.buildGetRequest(reg, get);
157       controller.reset();
158       controller.setCallTimeout(callTimeout);
159 
160       try {
161         ClientProtos.GetResponse response = getStub().get(controller, request);
162         if (response == null) {
163           return null;
164         }
165         return ProtobufUtil.toResult(response.getResult(), controller.cellScanner());
166       } catch (ServiceException se) {
167         throw ProtobufUtil.getRemoteException(se);
168       }
169     }
170 
171     @Override
172     public boolean isCancelled() {
173       return controller.isCanceled();
174     }
175   }
176 
177   /**
178    * <p>
179    * Algo:
180    * - we put the query into the execution pool.
181    * - after x ms, if we don't have a result, we add the queries for the secondary replicas
182    * - we take the first answer
183    * - when done, we cancel what's left. Cancelling means:
184    * - removing from the pool if the actual call was not started
185    * - interrupting the call if it has started
186    * Client side, we need to take into account
187    * - a call is not executed immediately after being put into the pool
188    * - a call is a thread. Let's not multiply the number of thread by the number of replicas.
189    * Server side, if we can cancel when it's still in the handler pool, it's much better, as a call
190    * can take some i/o.
191    * </p>
192    * Globally, the number of retries, timeout and so on still applies, but it's per replica,
193    * not global. We continue until all retries are done, or all timeouts are exceeded.
194    */
195   public Result call(int operationTimeout)
196       throws DoNotRetryIOException, InterruptedIOException, RetriesExhaustedException {
197     boolean isTargetReplicaSpecified = (get.getReplicaId() >= 0);
198 
199     RegionLocations rl = null;
200     boolean skipPrimary = false;
201     try {
202       rl = getRegionLocations(true,
203         (isTargetReplicaSpecified ? get.getReplicaId() : RegionReplicaUtil.DEFAULT_REPLICA_ID),
204         cConnection, tableName, get.getRow());
205     } catch (RetriesExhaustedException | DoNotRetryIOException e) {
206       // When there is no specific replica id specified. It just needs to load all replicas.
207       if (isTargetReplicaSpecified) {
208         throw e;
209       } else {
210         // We cannot get the primary replica location, it is possible that the region
211         // server hosting meta is down, it needs to proceed to try cached replicas.
212         if (cConnection instanceof ConnectionManager.HConnectionImplementation) {
213           rl = ((ConnectionManager.HConnectionImplementation)cConnection).getCachedLocation(
214               tableName, get.getRow());
215           if (rl == null) {
216             // No cached locations
217             throw e;
218           }
219 
220           // Primary replica location is not known, skip primary replica
221           skipPrimary = true;
222         } else {
223           // For completeness
224           throw e;
225         }
226       }
227     }
228 
229     final ResultBoundedCompletionService<Result> cs =
230         new ResultBoundedCompletionService<>(this.rpcRetryingCallerFactory, pool, rl.size());
231 
232     int startIndex = 0;
233     int endIndex = rl.size();
234 
235     if(isTargetReplicaSpecified) {
236       addCallsForReplica(cs, rl, get.getReplicaId(), get.getReplicaId());
237       endIndex = 1;
238     } else {
239       if (!skipPrimary) {
240         addCallsForReplica(cs, rl, 0, 0);
241         try {
242           // wait for the timeout to see whether the primary responds back
243           Future<Result> f = cs.poll(timeBeforeReplicas, TimeUnit.MICROSECONDS); // Yes, microseconds
244           if (f != null) {
245             return f.get(); //great we got a response
246           }
247           if (cConnection.getConnectionMetrics() != null) {
248             cConnection.getConnectionMetrics().incrHedgedReadOps();
249           }
250         } catch (ExecutionException e) {
251           // We ignore the ExecutionException and continue with the secondary replicas
252           if (LOG.isDebugEnabled()) {
253             LOG.debug("Primary replica returns " + e.getCause());
254           }
255 
256           // Skip the result from the primary as we know that there is something wrong
257           startIndex = 1;
258         } catch (CancellationException e) {
259           throw new InterruptedIOException();
260         } catch (InterruptedException e) {
261           throw new InterruptedIOException();
262         }
263       } else {
264         // Since primary replica is skipped, the endIndex needs to be adjusted accordingly
265         endIndex --;
266       }
267 
268       // submit call for the all of the secondaries at once
269       addCallsForReplica(cs, rl, 1, rl.size() - 1);
270     }
271 
272     try {
273       ResultBoundedCompletionService<Result>.QueueingFuture<Result> f =
274           cs.pollForFirstSuccessfullyCompletedTask(operationTimeout, TimeUnit.MILLISECONDS, startIndex, endIndex);
275       if (f == null) {
276         throw new RetriesExhaustedException("Timed out after " + operationTimeout +
277             "ms. Get is sent to replicas with startIndex: " + startIndex +
278             ", endIndex: " + endIndex + ", Locations: " + rl);
279       }
280       if (cConnection.getConnectionMetrics() != null && !isTargetReplicaSpecified &&
281           !skipPrimary && f.getReplicaId() != RegionReplicaUtil.DEFAULT_REPLICA_ID) {
282         cConnection.getConnectionMetrics().incrHedgedReadWin();
283       }
284       return f.get();
285     } catch (ExecutionException e) {
286       throwEnrichedException(e, retries);
287     } catch (CancellationException e) {
288       throw new InterruptedIOException();
289     } catch (InterruptedException e) {
290       throw new InterruptedIOException();
291     } finally {
292       // We get there because we were interrupted or because one or more of the
293       // calls succeeded or failed. In all case, we stop all our tasks.
294       cs.cancelAll();
295     }
296 
297     LOG.error("Imposible? Arrive at an unreachable line..."); // unreachable
298     return null; // unreachable
299   }
300 
301   /**
302    * Extract the real exception from the ExecutionException, and throws what makes more
303    * sense.
304    */
305   static void throwEnrichedException(ExecutionException e, int retries)
306       throws RetriesExhaustedException, DoNotRetryIOException {
307     Throwable t = e.getCause();
308     assert t != null; // That's what ExecutionException is about: holding an exception
309 
310     if (t instanceof RetriesExhaustedException) {
311       throw (RetriesExhaustedException) t;
312     }
313 
314     if (t instanceof DoNotRetryIOException) {
315       throw (DoNotRetryIOException) t;
316     }
317 
318     if (t instanceof NeedUnmanagedConnectionException) {
319       throw new DoNotRetryIOException(t);
320     }
321 
322     RetriesExhaustedException.ThrowableWithExtraContext qt =
323         new RetriesExhaustedException.ThrowableWithExtraContext(t,
324             EnvironmentEdgeManager.currentTime(), null);
325 
326     List<RetriesExhaustedException.ThrowableWithExtraContext> exceptions =
327         Collections.singletonList(qt);
328 
329     throw new RetriesExhaustedException(retries, exceptions);
330   }
331 
332   /**
333    * Creates the calls and submit them
334    *
335    * @param cs  - the completion service to use for submitting
336    * @param rl  - the region locations
337    * @param min - the id of the first replica, inclusive
338    * @param max - the id of the last replica, inclusive.
339    */
340   private void addCallsForReplica(ResultBoundedCompletionService<Result> cs,
341                                  RegionLocations rl, int min, int max) {
342     for (int id = min; id <= max; id++) {
343       HRegionLocation hrl = rl.getRegionLocation(id);
344       ReplicaRegionServerCallable callOnReplica = new ReplicaRegionServerCallable(id, hrl);
345       cs.submit(callOnReplica, callTimeout, id);
346     }
347   }
348 
349   static RegionLocations getRegionLocations(boolean useCache, int replicaId,
350                  ClusterConnection cConnection, TableName tableName, byte[] row)
351       throws RetriesExhaustedException, DoNotRetryIOException, InterruptedIOException {
352 
353     RegionLocations rl;
354     try {
355       if (useCache) {
356         rl = cConnection.locateRegion(tableName, row, true, true, replicaId);
357       } else {
358         rl = cConnection.relocateRegion(tableName, row, replicaId);
359       }
360     } catch (DoNotRetryIOException e) {
361       throw e;
362     } catch (NeedUnmanagedConnectionException e) {
363       throw new DoNotRetryIOException(e);
364     } catch (RetriesExhaustedException e) {
365       throw e;
366     } catch (InterruptedIOException e) {
367       throw e;
368     } catch (IOException e) {
369       throw new RetriesExhaustedException("Can't get the location for replica " + replicaId, e);
370     }
371     if (rl == null) {
372       throw new RetriesExhaustedException("Can't get the location for replica " + replicaId);
373     }
374 
375     return rl;
376   }
377 }