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.

6 comments:

Ed Burnette said...

There's another example of asynchronous HTTP network I/O in the book "Hello, Android". You can download the "Translate" example (and others) for free by clicking on the Code link at http://www.pragprog.com/titles/eband/hello-android . Enjoy.

Josh Guilfoyle said...

I just checked your code and it is not correct. interrupt() does not affect blocking I/O, as is mentioned in Java's documentation.

Try forcing your connect call to block by shutting down your external network (use iptables to stop packets coming from the destination), then try to interrupt your example and wait for the thread to join. You will notice that it takes up to the specified timeout to terminate.

The point of my example was to demonstrate a proper technique where you can immediately terminate a thread waiting on network I/O.

xuemei said...

I like play online game, I also buy ragnarok online zeny and ro zeny, the ragnarok zeny is very cheap, and use the iro zeny can buy many things, I like cheap zeny, thanks, it is very good.

I like play online game, I also buy rupees and rappelz rupees, the rappelz gold is very cheap, and use the rappelz money can buy many things, I like cheap rappelz rupees, thanks, it is very good.

xuemei said...

Now do you worried about that in the game do not had enough rohan crone to play the game, now you can not worried, my friend told me a website, in here you can buy a lot rohan gold and only spend a little money, do not hesitate, it was really, in here we had much rohan online crone, we can sure that you will get the rohan online gold, quick to come here to buy rohan money.

Now do you worried about that in the game do not had enough Rose zuly to play the game, now you can not worried, my friend told me a website, in here you can buy a lot rose zulie and only spend a little money, do not hesitate, it was really, in here we had much rose online zuly, we can sure that you will get the rose online zulie, quick to come here to buy Arua ROSE zuly.

Y.Ramesh Rao said...

This is in fact a great deal of Effort Man, It really was fruitful for me in solving my issue for canceling network Requests. Great Work, and works simply fine

Андрей said...

Why should we do the following: ????

synchronized(lock) {
mMethod = method;
}