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.Map.Entry;
22  import java.util.concurrent.ConcurrentHashMap;
23  import java.util.concurrent.ConcurrentMap;
24  
25  import org.apache.commons.lang.mutable.MutableBoolean;
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.apache.hadoop.conf.Configuration;
29  import org.apache.hadoop.hbase.HConstants;
30  import org.apache.hadoop.hbase.ServerName;
31  import org.apache.hadoop.hbase.classification.InterfaceAudience;
32  import org.apache.hadoop.hbase.exceptions.ClientExceptionsUtil;
33  import org.apache.hadoop.hbase.exceptions.PreemptiveFastFailException;
34  import org.apache.hadoop.hbase.ipc.CallTimeoutException;
35  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
36  import org.apache.hadoop.ipc.RemoteException;
37  
38  import com.google.common.annotations.VisibleForTesting;
39  
40  /**
41   * 
42   * The concrete {@link RetryingCallerInterceptor} class that implements the preemptive fast fail
43   * feature.
44   * 
45   * The motivation is as follows : 
46   * In case where a large number of clients try and talk to a particular region server in hbase, if
47   * the region server goes down due to network problems, we might end up in a scenario where
48   * the clients would go into a state where they all start to retry.
49   * This behavior will set off many of the threads in pretty much the same path and they all would be
50   * sleeping giving rise to a state where the client either needs to create more threads to send new
51   * requests to other hbase machines or block because the client cannot create anymore threads.
52   * 
53   * In most cases the clients might prefer to have a bound on the number of threads that are created
54   * in order to send requests to hbase. This would mostly result in the client thread starvation.
55   * 
56   *  To circumvent this problem, the approach that is being taken here under is to let 1 of the many
57   *  threads who are trying to contact the regionserver with connection problems and let the other
58   *  threads get a {@link PreemptiveFastFailException} so that they can move on and take other
59   *  requests.
60   *  
61   *  This would give the client more flexibility on the kind of action he would want to take in cases
62   *  where the regionserver is down. He can either discard the requests and send a nack upstream
63   *  faster or have an application level retry or buffer the requests up so as to send them down to
64   *  hbase later.
65   *
66   */
67  @InterfaceAudience.Private
68  class PreemptiveFastFailInterceptor extends RetryingCallerInterceptor {
69  
70    private static final Log LOG = LogFactory
71        .getLog(PreemptiveFastFailInterceptor.class);
72  
73    // amount of time to wait before we consider a server to be in fast fail
74    // mode
75    protected final long fastFailThresholdMilliSec;
76  
77    // Keeps track of failures when we cannot talk to a server. Helps in
78    // fast failing clients if the server is down for a long time.
79    protected final ConcurrentMap<ServerName, FailureInfo> repeatedFailuresMap =
80        new ConcurrentHashMap<ServerName, FailureInfo>();
81  
82    // We populate repeatedFailuresMap every time there is a failure. So, to
83    // keep it from growing unbounded, we garbage collect the failure information
84    // every cleanupInterval.
85    protected final long failureMapCleanupIntervalMilliSec;
86  
87    protected volatile long lastFailureMapCleanupTimeMilliSec;
88  
89    // clear failure Info. Used to clean out all entries.
90    // A safety valve, in case the client does not exit the
91    // fast fail mode for any reason.
92    private long fastFailClearingTimeMilliSec;
93  
94    private final ThreadLocal<MutableBoolean> threadRetryingInFastFailMode =
95        new ThreadLocal<MutableBoolean>();
96  
97    public PreemptiveFastFailInterceptor(Configuration conf) {
98      this.fastFailThresholdMilliSec = conf.getLong(
99          HConstants.HBASE_CLIENT_FAST_FAIL_THREASHOLD_MS,
100         HConstants.HBASE_CLIENT_FAST_FAIL_THREASHOLD_MS_DEFAULT);
101     this.failureMapCleanupIntervalMilliSec = conf.getLong(
102         HConstants.HBASE_CLIENT_FAST_FAIL_CLEANUP_MS_DURATION_MS,
103         HConstants.HBASE_CLIENT_FAST_FAIL_CLEANUP_DURATION_MS_DEFAULT);
104     lastFailureMapCleanupTimeMilliSec = EnvironmentEdgeManager.currentTime();
105   }
106 
107   public void intercept(FastFailInterceptorContext context)
108       throws PreemptiveFastFailException {
109     context.setFailureInfo(repeatedFailuresMap.get(context.getServer()));
110     if (inFastFailMode(context.getServer()) && !currentThreadInFastFailMode()) {
111       // In Fast-fail mode, all but one thread will fast fail. Check
112       // if we are that one chosen thread.
113       context.setRetryDespiteFastFailMode(shouldRetryInspiteOfFastFail(context
114           .getFailureInfo()));
115       if (!context.isRetryDespiteFastFailMode()) { // we don't have to retry
116         LOG.debug("Throwing PFFE : " + context.getFailureInfo() + " tries : "
117             + context.getTries());
118         throw new PreemptiveFastFailException(
119             context.getFailureInfo().numConsecutiveFailures.get(),
120             context.getFailureInfo().timeOfFirstFailureMilliSec,
121             context.getFailureInfo().timeOfLatestAttemptMilliSec, context.getServer(),
122             context.getGuaranteedClientSideOnly().isTrue());
123       }
124     }
125     context.setDidTry(true);
126   }
127 
128   public void handleFailure(FastFailInterceptorContext context,
129       Throwable t) throws IOException {
130     handleThrowable(t, context.getServer(),
131         context.getCouldNotCommunicateWithServer(),
132         context.getGuaranteedClientSideOnly());
133   }
134 
135   public void updateFailureInfo(FastFailInterceptorContext context) {
136     updateFailureInfoForServer(context.getServer(), context.getFailureInfo(),
137         context.didTry(), context.getCouldNotCommunicateWithServer()
138             .booleanValue(), context.isRetryDespiteFastFailMode());
139   }
140 
141   /**
142    * Handles failures encountered when communicating with a server.
143    *
144    * Updates the FailureInfo in repeatedFailuresMap to reflect the failure.
145    * Throws RepeatedConnectException if the client is in Fast fail mode.
146    *
147    * @param serverName
148    * @param t
149    *          - the throwable to be handled.
150    * @throws PreemptiveFastFailException
151    */
152   @VisibleForTesting
153   protected void handleFailureToServer(ServerName serverName, Throwable t) {
154     if (serverName == null || t == null) {
155       return;
156     }
157     long currentTime = EnvironmentEdgeManager.currentTime();
158     FailureInfo fInfo = repeatedFailuresMap.get(serverName);
159     if (fInfo == null) {
160       fInfo = new FailureInfo(currentTime);
161       FailureInfo oldfInfo = repeatedFailuresMap.putIfAbsent(serverName, fInfo);
162 
163       if (oldfInfo != null) {
164         fInfo = oldfInfo;
165       }
166     }
167     fInfo.timeOfLatestAttemptMilliSec = currentTime;
168     fInfo.numConsecutiveFailures.incrementAndGet();
169   }
170 
171   public void handleThrowable(Throwable t1, ServerName serverName,
172       MutableBoolean couldNotCommunicateWithServer,
173       MutableBoolean guaranteedClientSideOnly) throws IOException {
174     Throwable t2 = ClientExceptionsUtil.translatePFFE(t1);
175     boolean isLocalException = !(t2 instanceof RemoteException);
176 
177     if ((isLocalException && ClientExceptionsUtil.isConnectionException(t2))) {
178       couldNotCommunicateWithServer.setValue(true);
179       guaranteedClientSideOnly.setValue(!(t2 instanceof CallTimeoutException));
180       handleFailureToServer(serverName, t2);
181     }
182   }
183 
184   /**
185    * Occasionally cleans up unused information in repeatedFailuresMap.
186    *
187    * repeatedFailuresMap stores the failure information for all remote hosts
188    * that had failures. In order to avoid these from growing indefinitely,
189    * occassionallyCleanupFailureInformation() will clear these up once every
190    * cleanupInterval ms.
191    */
192   protected void occasionallyCleanupFailureInformation() {
193     long now = System.currentTimeMillis();
194     if (!(now > lastFailureMapCleanupTimeMilliSec
195         + failureMapCleanupIntervalMilliSec))
196       return;
197 
198     // remove entries that haven't been attempted in a while
199     // No synchronization needed. It is okay if multiple threads try to
200     // remove the entry again and again from a concurrent hash map.
201     StringBuilder sb = new StringBuilder();
202     for (Entry<ServerName, FailureInfo> entry : repeatedFailuresMap.entrySet()) {
203       if (now > entry.getValue().timeOfLatestAttemptMilliSec
204           + failureMapCleanupIntervalMilliSec) { // no recent failures
205         repeatedFailuresMap.remove(entry.getKey());
206       } else if (now > entry.getValue().timeOfFirstFailureMilliSec
207           + this.fastFailClearingTimeMilliSec) { // been failing for a long
208                                                  // time
209         LOG.error(entry.getKey()
210             + " been failing for a long time. clearing out."
211             + entry.getValue().toString());
212         repeatedFailuresMap.remove(entry.getKey());
213       } else {
214         sb.append(entry.getKey().toString()).append(" failing ")
215             .append(entry.getValue().toString()).append("\n");
216       }
217     }
218     if (sb.length() > 0) {
219       LOG.warn("Preemptive failure enabled for : " + sb.toString());
220     }
221     lastFailureMapCleanupTimeMilliSec = now;
222   }
223 
224   /**
225    * Checks to see if we are in the Fast fail mode for requests to the server.
226    *
227    * If a client is unable to contact a server for more than
228    * fastFailThresholdMilliSec the client will get into fast fail mode.
229    *
230    * @param server
231    * @return true if the client is in fast fail mode for the server.
232    */
233   private boolean inFastFailMode(ServerName server) {
234     FailureInfo fInfo = repeatedFailuresMap.get(server);
235     // if fInfo is null --> The server is considered good.
236     // If the server is bad, wait long enough to believe that the server is
237     // down.
238     return (fInfo != null &&
239         EnvironmentEdgeManager.currentTime() >
240           (fInfo.timeOfFirstFailureMilliSec + this.fastFailThresholdMilliSec));
241   }
242 
243   /**
244    * Checks to see if the current thread is already in FastFail mode for *some*
245    * server.
246    *
247    * @return true, if the thread is already in FF mode.
248    */
249   private boolean currentThreadInFastFailMode() {
250     return (this.threadRetryingInFastFailMode.get() != null && (this.threadRetryingInFastFailMode
251         .get().booleanValue() == true));
252   }
253 
254   /**
255    * Check to see if the client should try to connnect to the server, inspite of
256    * knowing that it is in the fast fail mode.
257    *
258    * The idea here is that we want just one client thread to be actively trying
259    * to reconnect, while all the other threads trying to reach the server will
260    * short circuit.
261    *
262    * @param fInfo
263    * @return true if the client should try to connect to the server.
264    */
265   protected boolean shouldRetryInspiteOfFastFail(FailureInfo fInfo) {
266     // We believe that the server is down, But, we want to have just one
267     // client
268     // actively trying to connect. If we are the chosen one, we will retry
269     // and not throw an exception.
270     if (fInfo != null
271         && fInfo.exclusivelyRetringInspiteOfFastFail.compareAndSet(false, true)) {
272       MutableBoolean threadAlreadyInFF = this.threadRetryingInFastFailMode
273           .get();
274       if (threadAlreadyInFF == null) {
275         threadAlreadyInFF = new MutableBoolean();
276         this.threadRetryingInFastFailMode.set(threadAlreadyInFF);
277       }
278       threadAlreadyInFF.setValue(true);
279       return true;
280     } else {
281       return false;
282     }
283   }
284 
285   /**
286    *
287    * This function updates the Failure info for a particular server after the
288    * attempt to 
289    *
290    * @param server
291    * @param fInfo
292    * @param couldNotCommunicate
293    * @param retryDespiteFastFailMode
294    */
295   private void updateFailureInfoForServer(ServerName server,
296       FailureInfo fInfo, boolean didTry, boolean couldNotCommunicate,
297       boolean retryDespiteFastFailMode) {
298     if (server == null || fInfo == null || didTry == false)
299       return;
300 
301     // If we were able to connect to the server, reset the failure
302     // information.
303     if (couldNotCommunicate == false) {
304       LOG.info("Clearing out PFFE for server " + server.getServerName());
305       repeatedFailuresMap.remove(server);
306     } else {
307       // update time of last attempt
308       long currentTime = System.currentTimeMillis();
309       fInfo.timeOfLatestAttemptMilliSec = currentTime;
310 
311       // Release the lock if we were retrying inspite of FastFail
312       if (retryDespiteFastFailMode) {
313         fInfo.exclusivelyRetringInspiteOfFastFail.set(false);
314         threadRetryingInFastFailMode.get().setValue(false);
315       }
316     }
317 
318     occasionallyCleanupFailureInformation();
319   }
320 
321   @Override
322   public void intercept(RetryingCallerInterceptorContext context)
323       throws PreemptiveFastFailException {
324     if (context instanceof FastFailInterceptorContext) {
325       intercept((FastFailInterceptorContext) context);
326     }
327   }
328 
329   @Override
330   public void handleFailure(RetryingCallerInterceptorContext context,
331       Throwable t) throws IOException {
332     if (context instanceof FastFailInterceptorContext) {
333       handleFailure((FastFailInterceptorContext) context, t);
334     }
335   }
336 
337   @Override
338   public void updateFailureInfo(RetryingCallerInterceptorContext context) {
339     if (context instanceof FastFailInterceptorContext) {
340       updateFailureInfo((FastFailInterceptorContext) context);
341     }
342   }
343 
344   @Override
345   public RetryingCallerInterceptorContext createEmptyContext() {
346     return new FastFailInterceptorContext();
347   }
348 
349   protected boolean isServerInFailureMap(ServerName serverName) {
350     return this.repeatedFailuresMap.containsKey(serverName);
351   }
352 
353   @Override
354   public String toString() {
355     return "PreemptiveFastFailInterceptor";
356   }
357 }