Community Tip - Learn all about the Community Ranking System, a fun gamification element of the PTC Community. X
Calling external services from M2M applications is a critical aspect of building end-to-end solutions. Knowing how to apply network timeouts when connecting to external servers can prevent unexpected and problematic network hang-ups. Let's investigate how to create a safe networking flow using HttpClient, HttpBuilder, and Apache’s FTPClient class.
Background
Custom Objects called from Expression Rules have a configurable maximum execution time. This is set by the com.axeda.drm.rules.statistics.rule-time-threshold property. Without this safeguard in place long running or misbehaved Custom Objects can cause internal processing queues to fill and the server will suffer a performance degradation.
In Java (and Groovy) all network calls internally use InputStream.read() to establish the socket connection and to read data from the socket. It is possible for faulty external servers (such as an FTP server) to hang and not properly respond. This means that the InputStream.read() method will continuously wait for the server to respond with data, and the server will never respond. According to the Java spec, InputStream.read() may be uninterruptable while it is waiting for data. This means that if a Custom Object has exceeded the com.axeda.drm.rules.statistics.rule-time-threshold the Rule Sniper will still not be able to interrupt the Custom Object’s execution if it is waiting on InputStream.read(). Because the Custom Object cannot be stopped, the internal processing queues will eventually fill.
Even though InputStream.read() is uninterruptable it is still possible to set timeouts for it to be able to give up on a connection. Beyond that, we want to make sure that the connection is completely disconnected.
Types of Timeouts
There are typically two types of timeouts that should be set when making calls over the web: the Connection Timeout and the Socket Timeout.
The Connection Timeout is the maximum amount of time that should be allowed when establishing the bi-directional socket connection between the client and server. Behind the scenes socket connection involves resolving the domain name of the server to an IP address, and then the server opening a port to connect with the client’s port.
The Socket Timeout is the timeout that limits the amount of time each socket operation is allowed to take. It limits the amount of time InputStream.read() will listen for a server’s response. If a server is faulty or overloaded it may take a long time (or forever) to respond to a request. This timeout limits the amount of time the client will wait for the server to respond.
When making any calls from a Custom Object to an external server (either making WebService calls, or FTP transfers), you should always set the Connection Timeout and the Socket Timeout. Always try to keep the timeouts as reasonably small as possible. Failure to do so could unexpectedly impact your Axeda server. Consider a Custom Object that takes an average of 10 seconds to run is called to make an external WebService call once a minute. This will not cause any issues and the system will be stable. If the external server suddenly has a performance degredation and now the external WebService call takes over a minute to run, the execution queue will eventually fill, causing performance degradation to the Axeda system. To protect against this scenario, set the timeouts to limit the call to one minute, and log whenever the time limit is exceeded.
Examples
Provided below are examples of properly set timeouts and thorough connection management use HttpClient, HttpBuilder, and FTPClient.
All of these examples assume they are being executed from Custom Objects.
By default, set the Connection Timeout to 10 seconds. In normal circumstances, connections should not take more then 10 seconds. If they are exceeding this time there is a good chance of networking issues between the client and server.
The Socket Timeout can vary per use-case. The examples provided set the Socket Timeout to 30 seconds, which should be sufficient for typical WebService calls and small to medium sized FTP file transfers. Depending exactly on what is being done, the timout may have to be increased. If you expect the call to go over 5 minutes please contact Axeda Support to investigate increasing com.axeda.drm.rules.statistics.rule-time-threshold property (which defaults to 5 minutes).
HttpClient
//HttpClient
import org.apache.http.client.HttpClient
import org.apache.http.impl.client.DefaultHttpClient
import org.apache.http.client.methods.HttpGet
import org.apache.http.HttpResponse
import org.apache.http.params.BasicHttpParams
import org.apache.http.params.HttpParams
import org.apache.http.params.HttpConnectionParams
int TENSECONDS = 10*1000
int THIRTYSECONDS = 30*1000
final HttpParams httpParams = new BasicHttpParams()
//Establishing the connection should take <10 seconds in most circumstances
HttpConnectionParams.setConnectionTimeout(httpParams, TENSECONDS)
//The data transfer/call should take <30 seconds. Adjust as necessary if receiving large data sets.
HttpConnectionParams.setSoTimeout(httpParams, THIRTYSECONDS)
HttpClient hc = new DefaultHttpClient(httpParams)
try {
//Simply get the contents of http://www.axeda.com and log it to the Custom Object Log
HttpGet get = new HttpGet("http://www.axeda.com")
HttpResponse response = hc.execute(get)
BufferedReader br = new BufferedReader( new InputStreamReader( response.getEntity().getContent()))
br.readLines().each {
logger.info it
}
} finally {
//Make sure to shutdown the connectionManager
hc.getConnectionManager().shutdown()
}
return true
HttpBuilder
import groovyx.net.http.HTTPBuilder
import static groovyx.net.http.ContentType.*
import static groovyx.net.http.Method.*
int TENSECONDS = 10*1000;
int THIRTYSECONDS = 30*1000;
HTTPBuilder builder = new HTTPBuilder('http://www.axeda.com')
//HTTPBuilder has no direct methods to add timeouts. We have to add them to the HttpParams of the underlying HttpClient
builder.getClient().getParams().setParameter("http.connection.timeout", new Integer(TENSECONDS))
builder.getClient().getParams().setParameter("http.socket.timeout", new Integer(THIRTYSECONDS))
try
{
//Simply get the contents of http://www.axeda.com and log it to the Custom Object Log
builder.request(GET, TEXT){
response.success = { resp, res ->
res.readLines().each {
logger.info it
}
}
}
}
finally
{
//Make sure to always shut down the HTTPBuilder when you’re done with it
builder.shutdown()
}
return true
FtpClient
Apache’s FTPClient has a third type of timeout, the Default Timeout. The Default Timeout is a timeout that further ensures that socket timeouts are always used. Note: Default Timeout does not set a timeout for the .connect() method.
import org.apache.commons.net.ftp.*
import java.io.InputStream
import java.io.ByteArrayInputStream
String ftphost = "127.0.0.1"
String ftpuser = "test"
String ftppwd = "test"
int ftpport = 21
String ftpDir = "tmp/FTP"
int TENSECONDS = 10*1000
int THIRTYSECONDS = 30*1000
//Declare FTP client
FTPClient ftp = new FTPClient()
try
{
ftp.setConnectTimeout(TENSECONDS)
ftp.setDefaultTimeout(TENSECONDS)
ftp.connect(ftphost, ftpport)
//30 seconds to log on. Also 30 seconds to change to working directory.
ftp.setSoTimeout(THIRTYSECONDS)
def reply = ftp.getReplyCode()
if (!FTPReply.isPositiveCompletion(reply)) {
throw new Exception("Unable to connect to FTP server")
}
if (!ftp.login(ftpuser, ftppwd)) {
throw new Exception("Unable to login to FTP server")
}
if (!ftp.changeWorkingDirectory(ftpDir)) {
throw new Exception("Unable to change working directory on FTP server")
}
//Change the timeout here for a large file transfer that will take over 30 seconds
//ftp.setSoTimeout(THIRTYSECONDS);
ftp.setFileType(FTPClient.ASCII_FILE_TYPE)
ftp.enterLocalPassiveMode()
String filetxt = "Some String file content"
InputStream is = new ByteArrayInputStream(filetxt.getBytes('US-ASCII'))
try {
if (!ftp.storeFile("myFile.txt", is)) {
throw new Exception("Unable to write file to FTP server")
}
} finally {
//Make sure to always close the inputStream
is.close()
}
}
catch(Exception e)
{
//handle exceptions here by logging or auditing
}
finally
{
//if the IO is timed out or force disconnected, exceptions may be thrown when trying to logout/disconnect
try
{
//10 seconds to log off. Also 10 seconds to disconnect.
ftp.setSoTimeout(TENSECONDS);
ftp.logout();
//depending on the state of the server the .logout() may throw an exception,
//we want to ensure complete disconnect.
}
catch(Exception innerException)
{
//You potentially just want to log that there was a logout exception.
}
finally
{
//Make sure to always disconnect. If not, there is a chance you will leave hanging sockects
ftp.disconnect();
}
}
return true