/**
* Copyright 2010 The Apache Software Foundation
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.wasp.client;
import com.alibaba.wasp.EntityGroupLocation;
import com.alibaba.wasp.FConstants;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.DoNotRetryIOException;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.ipc.RemoteException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
/**
* Abstract class that implements {@link java.util.concurrent.Callable}.
* Implementation stipulates return type and method we actually invoke on remote
* Server. Usually used inside a try/catch that fields usual connection failures
* all wrapped up in a retry loop.
* <p>
* Call {@link #connect(boolean)} to connect to server hosting entityGroup that
* contains the passed row in the passed table before invoking {@link #call()}.
*
* @see FConnection#getFServerWithoutRetries(ServerCallable)
* @param <T>
* the class that the ServerCallable handles
*/
public abstract class ServerCallable<T> implements Callable<T> {
public final Log LOG = LogFactory.getLog(this.getClass());
protected final FConnection connection;
protected final byte[] tableName;
protected final byte[] row;
protected EntityGroupLocation location;
protected ClientProtocol server;
protected int callTimeout;
protected long startTime, endTime;
protected long pause;
/**
* @param connection
* Connection to use.
* @param tableName
* Table name to which <code>row</code> belongs.
* @param row
* The row we want in <code>tableName</code>.
*/
public ServerCallable(FConnection connection, byte[] tableName, byte[] row) {
this(connection, tableName, row,
FConstants.DEFAULT_WASP_CLIENT_OPERATION_TIMEOUT);
}
public ServerCallable(FConnection connection, byte[] tableName, byte[] row,
int callTimeout) {
this.connection = connection;
this.tableName = tableName;
this.row = row;
this.callTimeout = callTimeout;
this.pause = connection.getConfiguration().getLong(FConstants.WASP_CLIENT_PAUSE,
FConstants.DEFAULT_WASP_CLIENT_PAUSE);
}
/**
* Connect to the server hosting entityGroup with row from table name.
*
* @param reload
* Set this to true if connection should re-find the entityGroup
* @throws java.io.IOException
* e
*/
public void connect(final boolean reload) throws IOException {
this.location = connection.getEntityGroupLocation(tableName, row, reload);
this.server = connection.getClient(location.getHostname(),
location.getPort());
}
public void beforeCall() {
this.startTime = System.currentTimeMillis();
}
public void afterCall() {
this.endTime = System.currentTimeMillis();
}
public void shouldRetry(Throwable throwable) throws IOException {
if (this.callTimeout != FConstants.DEFAULT_WASP_CLIENT_OPERATION_TIMEOUT)
if (throwable instanceof SocketTimeoutException
|| (this.endTime - this.startTime > this.callTimeout)) {
throw (SocketTimeoutException) (SocketTimeoutException) new SocketTimeoutException(
"Call to access row '" + Bytes.toString(row) + "' on table '"
+ Bytes.toString(tableName)
+ "' failed on socket timeout exception: " + throwable)
.initCause(throwable);
} else {
this.callTimeout = ((int) (this.endTime - this.startTime));
}
}
/**
* @return {@link FConnection} instance used by this Callable.
*/
FConnection getConnection() {
return this.connection;
}
/**
* Run this instance with retries, timed waits, and refinds of missing
* entityGroups.
*
* @return an object of type T
* @throws java.io.IOException
* if a remote or network exception occurs
* @throws RuntimeException
* other unspecified error
*/
public T withRetries() throws IOException, RuntimeException {
Configuration c = getConnection().getConfiguration();
final int numRetries = c.getInt(FConstants.WASP_CLIENT_RETRIES_NUMBER,
FConstants.DEFAULT_WASP_CLIENT_RETRIES_NUMBER);
List<RetriesExhaustedException.ThrowableWithExtraContext> exceptions = new ArrayList<RetriesExhaustedException.ThrowableWithExtraContext>();
for (int tries = 0; tries < numRetries; tries++) {
try {
beforeCall();
connect(tries != 0);
return call();
} catch (Throwable t) {
shouldRetry(t);
t = translateException(t);
if (t instanceof SocketTimeoutException
|| t instanceof ConnectException
|| t instanceof RetriesExhaustedException) {
// if thrown these exceptions, we clear all the cache entries that
// map to that slow/dead server; otherwise, let cache miss and ask
// .FMETA. again to find the new location
EntityGroupLocation egl = location;
if (egl != null) {
getConnection().clearCaches(egl.getHostnamePort());
}
}
if(t instanceof RuntimeException) {
throw new RuntimeException(t);
}
RetriesExhaustedException.ThrowableWithExtraContext qt = new RetriesExhaustedException.ThrowableWithExtraContext(
t, System.currentTimeMillis(), toString());
exceptions.add(qt);
long pauseTime = ConnectionUtils.getPauseTime(this.pause, tries);
LOG.info("withRetries attempt " + tries + " of " + numRetries
+ " failed; retrying after sleep of " + pauseTime);
try {
Thread.sleep(pauseTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(
"Thread was interrupted while trying to connect to FServer.", e);
}
if (tries == numRetries - 1) {
throw new RetriesExhaustedException(tries, exceptions);
}
} finally {
afterCall();
}
}
return null;
}
/**
* Run this instance against the server once.
*
* @return an object of type T
* @throws java.io.IOException
* if a remote or network exception occurs
* @throws RuntimeException
* other unspecified error
*/
public T withoutRetries() throws IOException, RuntimeException {
try {
beforeCall();
connect(false);
return call();
} catch (Throwable t) {
Throwable t2 = translateException(t);
if (t2 instanceof IOException) {
throw (IOException) t2;
} else {
throw new RuntimeException(t2);
}
} finally {
afterCall();
}
}
private static Throwable translateException(Throwable t) throws IOException {
if (t instanceof UndeclaredThrowableException) {
t = t.getCause();
}
if (t instanceof RemoteException) {
t = ((RemoteException) t).unwrapRemoteException();
}
if (t instanceof DoNotRetryIOException) {
throw (DoNotRetryIOException) t;
}
if(t instanceof IOException) {
RuntimeException runtimeException = unwrapRuntimeException(t);
if(runtimeException != null) {
return runtimeException;
}
}
return t;
}
private static RuntimeException unwrapRuntimeException(Throwable t) {
if(StringUtils.isNotEmpty(t.getMessage())) {
try {
Class exceptionClass = Class.forName(t.getMessage());
Constructor cn = exceptionClass.getConstructor(String.class);
cn.setAccessible(true);
String firstLine = t.getMessage();
Object ex = cn.newInstance(firstLine);
if(ex instanceof RuntimeException) {
return (RuntimeException)ex;
}
} catch (ClassNotFoundException e) {
//ignore
} catch (NoSuchMethodException e) {
//ignore
} catch (InvocationTargetException e) {
//ignore
} catch (InstantiationException e) {
//ignore
} catch (IllegalAccessException e) {
//ignore
}
}
return null;
}
}