Thursday, July 31, 2008

Interruptible I/O example using HttpClient

Recently I found myself considering some of the gotchas of threaded, blocking I/O as I've been using it on Android. Specifically, how do we gracefully handle interruptions demanded by the user or the system to free resources? After some thought there seems to be three basic strategies with Java:
  • 1. Use non-blocking I/O, which is generally clumsy and unintuitive for most Java engineers.
  • 2. Cancel blocking I/O threads by simply setting a stop flag and discarding the reference to the thread. The thread will clean itself up after an arbitrary length of time up to its transmit or connect timeout.
  • 3. Close the socket owning the input stream on which the blocking thread is waiting.
It seems that #2 is the most popular choice on Android however I would like to make a case for #3 as a cleaner method of tidying resources on user request. With this approach, we can be certain to quickly close any open files, relinquish database, object, or file locks, and allow the thread to clean up its resources quickly. As it turns out, using HttpClient makes this approach relatively painless, but there are a few gotchas in this pattern that we must watch out for.

So, to get started we need to create our StoppableDownloadThread class which allows us to encapsulate our interrupt logic.
public class StoppableDownloadThread extends Thread
{
private String mURL;

private HttpGet mMethod = null;

/* Volatile stop flag used to coordinate state between the two
* threads involved in this example. */
protected volatile boolean mStopped = false;

/* Synchronizes access to mMethod to prevent an unlikely race
* condition when stopDownload() is called before mMethod has
been committed. */
private Object lock = new Object();

public StoppableDownloadThread(String url)
{
mURL = url;
}
}
This simply outlines our basic strategy for stopping and synchronization. A simple volatile boolean flag and a monitor lock to share the HttpGet handle should do just fine. Now let's continue with the implementation of the run() method:
public void run()
{
HttpClient cli = new DefaultHttpClient();
HttpGet method;

try {
method = new HttpGet(mURL);
} catch (URISyntaxException e) {
e.printStackTrace();
return;
}

/* It's important that we pause here to check if we've been stopped
* already. Otherwise, we would happily progress, seemingly ignoring
* the stop request. */
if (mStopped == true)
return;

synchronized(lock) {
mMethod = method;
}

HttpResponse resp = null;
HttpEntity ent = null;
InputStream in = null;

try {
resp = cli.execute(mMethod);

if (mStopped == true)
return;

StatusLine status = resp.getStatusLine();

if ((ent = resp.getEntity()) != null)
{
long len;
if ((len = ent.getContentLength()) >= 0)
mHandler.sendSetLength(len);

in = ent.getContent();

byte[] b = new byte[2048];
int n;
long bytes = 0;

/* Note that for most applications, sending a handler message
* after each read() would be unnecessary. Instead, a timed
* approach should be utilized to send a message at most every
* x seconds. */
while ((n = in.read(b)) >= 0)
{
bytes += n;
System.out.println("Read " + bytes + " bytes...");
}
}
} catch (Exception e) {
/* We expect a SocketException on cancellation. Any other type of
* exception that occurs during cancellation is ignored regardless
* as there would be no need to handle it. */
if (mStopped == false)
e.printStackTrace();
} finally {
if (in != null)
try { in.close(); } catch (IOException e) {}

synchronized(lock) {
mMethod = null;
}

/* Close the socket (if it's still open) and cleanup. */
cli.getConnectionManager().shutdown();
}
}
This is a pretty standard example of an HTTP GET using HttpClient4, however do note that we have strategically placed checks against our stop flag to avoid leaving the download thread in an inconsistent state when it's being cancelled. We're not done yet though as we still need to implement the stop part of the interface so that our main thread (or any other thread) can abort the download thread:
public void stopDownload()
{
if (mStopped == true)
return;

/* Flag to instruct the downloading thread to halt at the next
* opportunity. */
mStopped = true;

/* Interrupt the blocking thread. This won't break out of a blocking
* I/O request, but will break out of a wait or sleep call. While in
* this case we know that no such condition is possible, it is always a
* good idea to include an interrupt to avoid assumptions about the
* thread in question. */
interrupt();

/* A synchronized lock is necessary to avoid catching mMethod in
* an uncommitted state from the download thread. */
synchronized(lock) {
/* This closes the socket handling our blocking I/O, which will
* interrupt the request immediately. This is not the same as
* closing the InputStream yieled by HttpEntity#getContent, as the
* stream is synchronized in such a way that would starve our main
* thread. */
if (mMethod != null)
mMethod.abort();
}
}
This completes our basic interface, but we still don't have a usable example here. There's no communication between our download thread back to the user in any meaningful way as would be required for an Android application. For that I have modified the above code slightly and introduced an Android layer in the form of a working demo. Source code for the full example: CancelHttpGet.tar.gz.