/**
* VMware Continuent Tungsten Replicator
* Copyright (C) 2015 VMware, Inc. All rights reserved.
*
* Licensed 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.
*
* Initial developer(s): Csaba Endre Simon
* Contributor(s):
*/
package com.continuent.tungsten.common.network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import org.apache.log4j.Logger;
import com.continuent.tungsten.common.config.TungstenProperties;
/**
* Tests for reachability using the TCP echo protocol.
*
* @author <a href="mailto:csimon@vmware.com">Csaba Endre Simon</a>
*/
public class Echo
{
private static Logger logger = Logger.getLogger(Echo.class);
private final static String message = "Hello";
public static final String TIME_TO_CONNECT_MS = "TimeToConnectMs";
public static final String TIME_TO_SEND_MS = "TimeToSendMs";
public static final String TIME_TO_RECEIVE_MS = "TimeToReceiveMs";
public static final String STATUS_KEY = "Status";
public static final String STATUS_MESSAGE_KEY = "StatusMsg";
public static final String STATUS_EXCEPTION = "Exception";
public static final String SOCKET_PHASE_CONNECT = "connecting to";
public static final String SOCKET_PHASE_RECEIVE = "reading from";
public static final String SOCKET_PHASE_WRITE = "writing to";
// Modifying the order here and in cluster-home/bin/tping bash script should
// be synchronous
public enum EchoStatus
{
OK, OPEN_FILE_LIMIT_ERROR, SOCKET_NO_IO, SOCKET_CONNECT_TIMEOUT, SEND_MESSAGE_TIMEOUT, RECEIVE_MESSAGE_TIMEOUT, MESSAGE_CORRUPT, SOCKET_IO_ERROR, HOST_IS_DOWN, NO_ROUTE_TO_HOST, UNKNOWN_HOST
}
/**
* Tests a host for reachability.
*
* @param hostName The host name of the echo server
* @param portNumber The port number of the echo server
* @param timeout Timeout in milliseconds
* @return the result wrapped inside tungsten properties.
*/
public static TungstenProperties isReachable(String hostName,
int portNumber, int timeout)
{
TungstenProperties statusAndResult = new TungstenProperties();
String statusMessage = null;
Socket socket = null;
InputStream socketInput = null;
OutputStream socketOutput = null;
String socketPhase = SOCKET_PHASE_CONNECT;
EchoStatus timeoutPhase = EchoStatus.SOCKET_CONNECT_TIMEOUT;
try
{
SocketAddress sockaddr = new InetSocketAddress(hostName, portNumber);
socket = new Socket();
socket.setSoTimeout(timeout);
socket.setReuseAddress(true);
long beforeConnect = System.currentTimeMillis();
socket.connect(sockaddr, timeout);
long timeToConnectMs = System.currentTimeMillis() - beforeConnect;
statusAndResult.setLong(TIME_TO_CONNECT_MS, timeToConnectMs);
socketInput = socket.getInputStream();
socketOutput = socket.getOutputStream();
if (socketInput == null || socketOutput == null)
{
statusMessage = String
.format("Socket connect error: InputStream=%s, OutputStream=%s after connect to %s:%s",
socketInput, socketOutput, hostName, portNumber);
statusAndResult.setObject(STATUS_KEY, EchoStatus.SOCKET_NO_IO);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
int timeLeft = (int) (timeout - timeToConnectMs);
if (timeLeft <= 0)
{
statusMessage = String
.format("Timeout while connecting: %d ms exceeds allowed timeout of %d ms.",
timeToConnectMs, timeout);
statusAndResult.setObject(STATUS_KEY,
EchoStatus.SOCKET_CONNECT_TIMEOUT);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
socket.setSoTimeout(timeLeft);
long beforeSend = System.currentTimeMillis();
socketPhase = SOCKET_PHASE_WRITE;
timeoutPhase = EchoStatus.SEND_MESSAGE_TIMEOUT;
byte[] outBuff = message.getBytes();
socketOutput.write(outBuff, 0, outBuff.length);
long timeToSendMs = System.currentTimeMillis() - beforeSend;
statusAndResult.setLong(TIME_TO_SEND_MS, timeToSendMs);
timeLeft = (int) (timeLeft - timeToSendMs);
if (timeLeft <= 0)
{
statusMessage = String
.format("Timeout while sending: %d ms exceeds allowed timeout of %d ms.",
timeToSendMs, timeLeft);
statusAndResult.setObject(STATUS_KEY,
EchoStatus.SEND_MESSAGE_TIMEOUT);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
socket.setSoTimeout(timeLeft);
long beforeReceive = System.currentTimeMillis();
socketPhase = SOCKET_PHASE_RECEIVE;
timeoutPhase = EchoStatus.RECEIVE_MESSAGE_TIMEOUT;
byte[] inBuff = new byte[outBuff.length];
int offset = 0;
int length = 0;
while (offset < inBuff.length)
{
length = socketInput.read(inBuff, offset, inBuff.length
- offset);
offset += length;
}
String echoMessage = new String(inBuff);
long timeToReceiveMs = System.currentTimeMillis() - beforeReceive;
statusAndResult.setLong(TIME_TO_RECEIVE_MS, timeToReceiveMs);
timeLeft = (int) (timeLeft - timeToReceiveMs);
if (timeLeft <= 0)
{
statusMessage = String
.format("Timeout while reading: %d ms exceeds allowed timeout of %d ms.",
timeToReceiveMs, timeLeft);
statusAndResult.setObject(STATUS_KEY,
EchoStatus.RECEIVE_MESSAGE_TIMEOUT);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
if (!message.equals(echoMessage))
{
statusMessage = String
.format("Corrupted message: expected '%s' with len=%d but got '%s' with len=%d.",
message, message.length(), echoMessage,
echoMessage.length());
statusAndResult.setObject(STATUS_KEY,
EchoStatus.MESSAGE_CORRUPT);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
statusMessage = String.format("Ping to %s:%d succeeded.", hostName,
portNumber);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
statusAndResult.setObject(STATUS_KEY, EchoStatus.OK);
return logAndReturnProperties(statusAndResult);
}
catch (SocketTimeoutException so)
{
statusMessage = String.format(
"Socket timeout while %s a socket %s:%d\nException='%s'",
socketPhase, hostName, portNumber, so);
statusAndResult.setObject(STATUS_KEY, timeoutPhase);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
statusAndResult.setObject(STATUS_EXCEPTION, so);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
catch (IOException ioe)
{
if ("Host is down".toLowerCase().contains(
ioe.getMessage().toLowerCase()))
{
statusMessage = String
.format("Host '%s' is down detected while %s a socket to %s:%d\nException='%s'",
hostName, socketPhase, hostName, portNumber,
ioe);
statusAndResult.setObject(STATUS_KEY, EchoStatus.HOST_IS_DOWN);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
statusAndResult.setObject(STATUS_EXCEPTION, ioe);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
if ("No route to host".toLowerCase().contains(
ioe.getMessage().toLowerCase()))
{
statusMessage = String
.format("No route to host '%s' detected while %s a socket to %s:%d\nException='%s'",
hostName, socketPhase, hostName, portNumber,
ioe);
statusAndResult.setObject(STATUS_KEY,
EchoStatus.NO_ROUTE_TO_HOST);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
statusAndResult.setObject(STATUS_EXCEPTION, ioe);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
if (ioe.getMessage().toLowerCase()
.contains("cannot assign requested address"))
{
statusMessage = String
.format("I/O exception while %s a socket to %s:%d\nException='%s'\n"
+ "Your open file limit may be too low. Check with 'ulimit -n' and increase if necessary.",
socketPhase, hostName, portNumber, ioe);
statusAndResult.setObject(STATUS_KEY,
EchoStatus.OPEN_FILE_LIMIT_ERROR);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
statusAndResult.setObject(STATUS_EXCEPTION, ioe);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
if (ioe.toString().contains("java.net.UnknownHostException"))
{
statusMessage = String
.format("I/O exception while %s a socket to %s:%d\nException='%s'\n"
+ "There may be an issue with your DNS for this host or your /etc/hosts entry is not correct.",
socketPhase, hostName, portNumber, ioe);
statusAndResult.setObject(STATUS_KEY, EchoStatus.UNKNOWN_HOST);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
statusAndResult.setObject(STATUS_EXCEPTION, ioe);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
statusMessage = String
.format("I/O exception caught while %s a socket to %s:%d\nException='%s'",
socketPhase, hostName, portNumber, ioe);
statusAndResult.setObject(STATUS_KEY, EchoStatus.SOCKET_IO_ERROR);
statusAndResult.setString(STATUS_MESSAGE_KEY, statusMessage);
statusAndResult.setObject(STATUS_EXCEPTION, ioe);
logger.warn(formatExecStatus(statusAndResult));
return logAndReturnProperties(statusAndResult);
}
finally
{
if (socketOutput != null)
{
try
{
socketOutput.close();
}
catch (Exception ignored)
{
}
finally
{
socketOutput = null;
}
}
if (socketInput != null)
{
try
{
socketInput.close();
}
catch (Exception ignored)
{
}
finally
{
socketInput = null;
}
}
if (socket != null)
{
try
{
socket.close();
}
catch (IOException i)
{
logger.warn("Exception while closing socket", i);
}
finally
{
socket = null;
}
}
}
}
/**
* Formats a TungstenProperties for human-friendly output
*
* @param props The tungsten properties to format
*/
private static String formatExecStatus(TungstenProperties props)
{
EchoStatus echoStatus = (EchoStatus) props.getObject(STATUS_KEY);
String statusMessage = props.getString(STATUS_MESSAGE_KEY);
return String.format("%s\n%s", echoStatus.toString(), statusMessage);
}
/**
* Log the given TungstenProperties
*
* @param props The tungsten properties to log
*/
private static TungstenProperties logAndReturnProperties(
TungstenProperties props)
{
if (logger.isTraceEnabled())
{
logger.trace(props);
}
return props;
}
/**
* Main method to permit external invocation.
*
* @param argv The program arguments.
*/
public static void main(String argv[])
{
if (argv.length != 3)
{
System.out.println("Tungsten ping utility");
System.out.println("Usage: tping hostname port timeout");
System.out.println(" timeout is in milliseconds");
System.exit(1);
}
else
{
try
{
String hostName = argv[0];
int portNumber = Integer.parseInt(argv[1]);
int timeout = Integer.parseInt(argv[2]);
TungstenProperties result = isReachable(hostName, portNumber,
timeout);
if (result.getObject(Echo.STATUS_KEY) == EchoStatus.OK)
{
System.exit(0);
}
else
{
EchoStatus echoStatus = (EchoStatus) result
.getObject(STATUS_KEY);
System.exit(1 + echoStatus.ordinal());
}
}
catch (NumberFormatException e)
{
System.out.println("Error parsing number: " + e.getMessage());
System.exit(1);
}
}
}
}