Ruby Timeout problem – the cheaters way out ….

There is (still) a general issue with Timeout’s in Ruby, relating to its use of Thread.kill etc, see this link for details.

Unfortunately its an issue in JRuby too.  The solutions suggested in the link are quite low-level (and no other high option seems to be exposed).

I have some code that uses Net::HTTP that we need to set timeouts on (we need to know if the other end has a problem) which under load, hits the above issue – we start running out of resources (too many open files).  Being a lazy/pragmatic programmer and having the advantage of working with JRuby – I decided to cheat and switch to a Java library that solves the timeout problem properly.  Meet my friend, httpcomponents (formerly httpclient)

I tried to use it directly in Ruby, but that gets messy, so I wrapped it up a bit:


package com.x;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.params.SyncBasicHttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
* User: kimptonc
* Date: 09/01/13
* Time: 12:05
* Thin wrapper around HttpClient lib - used by the ruby code
*/
public class HttpClient {

	private String host;
	private int port;
	private String user;
	private String pass;
	private DefaultHttpClient httpclient;
	private HttpHost targetHost;
	private BasicAuthCache authCache;
	private int conn_timeout;
	private int so_timeout;
	private static Object lock = new Object();

	public HttpClient(String host, int port, String user, String pass, int conn_timeout, int so_timeout) {
		this.host = host;
		this.port = port;
		this.user = user;
		this.pass = pass;
		this.conn_timeout = conn_timeout;
		this.so_timeout = so_timeout;

		while (httpclient == null)
		{
			synchronized (lock) {
				connect();
			}
		}

	}

	public Map post(String url, String requestBody) throws Exception
	{
		try {
			while (httpclient == null)
			{
				synchronized (lock) {
					connect();
				}
			}
			HttpPost post = new HttpPost(url);

			StringEntity requestEntity = new StringEntity(requestBody);
			requestEntity.setContentType("application/xml");
			post.setEntity(requestEntity);

			// Add AuthCache to the execution context

			BasicHttpContext localcontext = new BasicHttpContext();
			localcontext.setAttribute(ClientContext.AUTH_CACHE, authCache);

			HttpResponse response = httpclient.execute(targetHost, post, localcontext);
			HttpEntity entity = response.getEntity();

			Map responseMap = new HashMap();
			String responseBody = EntityUtils.toString(entity);
			responseMap.put("body",responseBody);
			responseMap.put("status",response.getStatusLine());

			EntityUtils.consume(entity); // needed?

			return responseMap;
		} catch (Exception e) {
			httpclient.getConnectionManager().shutdown();
			httpclient = null;
			throw e;
		}
	}

	private void connect() {
		if (httpclient != null)
			return; // looks like we are connected, so ignore

		targetHost = new HttpHost(host, port, "http");

		HttpParams params = new SyncBasicHttpParams();
		params
			.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, so_timeout)
			.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, conn_timeout);

		httpclient = new DefaultHttpClient(params);

		httpclient.getCredentialsProvider().setCredentials(
		new AuthScope(targetHost.getHostName(), targetHost.getPort()),
		new UsernamePasswordCredentials(user, pass));

		// Create AuthCache instance
		authCache = new BasicAuthCache();
		// Generate BASIC scheme object and add it to the local
		// auth cache
		BasicScheme basicAuth = new BasicScheme();
		authCache.put(targetHost, basicAuth);

		// for testing against a proxy in dev (eg Charles)
		// HttpHost proxy = new HttpHost("localhost", 8888);
		// httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);


	}

	public static void main(String[] args) throws Exception {
		HttpClient hc = new HttpClient("localhost", 8080,"one", "two",1,1);
		Map resp = hc.post("/v1/bond","<bond/>");
		System.out.println("Resp:"+resp);
	}
}

Then to use it in the Ruby, code its just this:


@http_client = com.x.HttpClient.new(@svc_host, @svc_port.to_i,@svc_user,@svc_pass, open_timeout.to_i, read_timeout.to_i)

resp = @http_client.post @url_path, xml

And it works – when we put this under load, we dont run out of resources :)