Use PooledExecutor to Prevent Flooding Server with Remote XML-RPC Calls copyright 2003 by Michael Isbell
THE PROBLEM. For simplicity's sake, let's say there are two sets of widgets on a GUI form:
a) a JList filled with tasks from a
todo list. Let's say there are twenty
tasks in the list. (All right, so it's a set of one.)
b) a set of JTextField widgets.
This works simply enough: when you pick an item in the JList, the JTextField widgets fill with the task's detail information: due dates, assignments, resources and so on.
Behind the scenes, whenever you pick a task from the list, somewhere along the line an XML-RPC call is being fired off over the Internet to a remote server, returning data into a data structure, which your code then parses to fill the JTextFields.
So what's the problem here? To put it another way,
(a)
why is the fact that this program is firing off calls remotely significant,
(b) what problems does it create,
and
(c) what do we have to do
differently to address those problems?
The answer is easy, but you need a fairly sophisticated understanding of everything that's going on to understand why the solution works. So let's tackle these issues one at a time.
(a) That the system issues remote calls to retrieve its data, rather than local calls, is significant because there's potentially no control over the number of calls that can be fired off to the remote server. The server usually has finite capacity, which, combined with a potentially large number of remote accesses creates the problem that (b) potentially many users could use the application and fire off large numbers of XML-RPC requests over the Internet, swamping the server in what is in effect a built-in DoS attack. This is just from scrolling up and down a JList!
Let's look at this in some detail. Let's start by looking at the way the GUI fills the JTextField widgets from the JList.
When you have a standard JList widget on a GUI form, there are two ways to treat the remote calls: synchronously or asynchronously. In other words there are two ways the list box can behave:
1. When you land on an item, the XML-RPC call is made synchronously as a method call in the valueChanged() method attached to the JList. When this is the case you can't scroll to another item (or do much else) until the XML-RPC call returns with data or times out, since you're code is running on the GUI's single thread. Remember, all code executed in an event listener method is executed on Swing's Event Dispatch Thread.
Let's remember for a moment that an RPC call, any RPC call, is by definition a synchronous call. When the remote request is sent over the socket, the socket sits and waits for a response until it arrives. If this request is made in a single-threaded application, the behavior of the application is that no other instruction will be executed until the data is returned. So, unless you place the remote call in another thread, everything stops until that call receives a response or times out.
2. When you land on an item, the XML-RPC call is made asynchronously, by firing off a separate thread from the valueChanged() method. To the GUI user, this means he can scroll up and down at will.
Having said that, there are then two ways that an asynchronous call can behave.
2i) as the list items are selected one by one as the user rapidly scrolls over them, EVERY SELECTED ITEM fires off an XML-RPC call asynchronously. Therefore, one by one, the calls return data and the code executes to fill the data fields related to selected item. If the user is racing his selector up and down the list box, testing to see how many calls he can fire off before he chokes the system, he'll see wildly erratic behavior: flickering data filling fields faster than the eye can follow. Finally, the data "catches up" with the last selected item in the list.
OR
2ii) as the list items are selected one by one as the user rapidly scrolls over them, ONLY THE FIRST SELECTION fires off an XML-RPC call asynchronously. After that, the listbox "magically" knows the the user is wasting its time, and it waits patiently for the user to stop screwing around. Then, when the user has selected the last item (when he's stopped playing with the list box), then, and only then, does the listbox permit the execution of the SECOND XML-RPC CALL, the one that fills the data fields with information related to the (finally) selected item.
A second reminder about Swing's single-thread architecture: if the Swing GUI code has been written correctly, all the XML-RPC threads paint the returned data using methods that place the Runnable objects containing the paint commands onto the Swing Event Queue. Then, one at a time,each Runnable object is dequeued and the Swing Event Dispatch Thread calls each Runnable object's run() method, which contains the command that actually assigns the data returned by the XML-RPC call to its appropriate widget (usually a widget.set(data) call of some kind). Remember that Swing widgets are NOT thread-safe, so when you work with asynchronous threads you still have to play by Swing's rules.
Again, the behavior of this simple example is significant because of its potential for swamping a server with XML-RPC calls simply by scrolling down a list, an activity you wouldn't think twice about in a local application!
BACK TO THE PROBLEM
Well, in the process of analyzing the significance of the example's behavior (a), we also identified the problem it created (b). So let's move on to look at (c). What do we have to do in our application to achieve the desired behavior? Plainly, the behavior in 2ii is the desired behavior for the application, in two ways: first, we don't flood the client application with bacchanalian quantities of wildly flickering data; second, we don't flood the server on the other end, drowning it in pointless XML-RPC requests, wasting server and network capacity to return data that will never be seen.
Let's walk through the way you want things to work on a detail level. After that, I'll show you how to use PooledExecutor to deal with the uncontrolled message generation problem effectively.
Imagine that the user has selected an item in the JList. Then he presses the down arrow and holds it down as the JList items flash by. Then, just as the selector slams into the bottom of the JList, his finger jumps to the up arrow, jams that key down and holds it there. Once more the items roll by at top speed. At the top of the list once more, the user's finger jumps back to the down arrow, presses it and holds it down and...you get the idea. We have a user who is "testing the system!"
Now, let's restate once more the behavior we'd like to see here.
1. When the user picks the first selection, valueChanged() fires off a thread which retrieves data.
2. When the user starts his high-speed scrolling, the first selection causes valueChanged() to fire off a thread and retrieve data.
3. There after, valueChanged() fires off threads, but they don't do anything. The application knows that we don't really want to run threads or retrieve data, we're just traversing a list.
4. Finally, the user lifts his finger from the key and the selector stops on a final selection. Once more, the thread fired off by JList's valueChanged() takes effect and retrieves data.
How do we create this behavior? In fact, we can't make it work exactly as I've described, but we can come close enough--that is, we can make sure that no more than three threads running XML-RPC requests are ever active: one will be running, one will be queued to run, and one will be blocked, and any other thread that tries to muscle its way into the process will be discarded. We can make this happen by using the PooledExecutor class, part of the util.concurrent package.
It's no secret that Java's implementation of concurrency keywords is inadequate for most sophisticated multi-threaded applications (cf. Holub for a fine critical analysis). Fortunately, Doug Lea, a professor in in Oswego, NY, has written a library of standard concurrency data structures that address a number of the problems and limitations of the Java concurrency keywords (cf. Lea, TS-2012 at java.sun.com/learning). Doug is largely responsible for JSR-166, which addresses many of Java's concurrency problems. In fact util.concurrent is so well done it's being integrated into the J2SDK 1.5 release, the release known as "Tiger." From the time Tiger is released, the tools in util.concurrent tools will be the way concurrency is handled in Java applications.
At the top level of the hierarchy in the util.concurrent package, there are three interfaces:
Sync
supports Semaphore and Mutex locking mechanisms
Queue
supports Putting objects on a channel and Taking objects from a channel
usually implemented as a BoundedBuffer or a QueuedChannel
Executor
runs Runnable objects.
usually implemented as QueuedExecutor, ThreadedExecutor or PooledExecutor.
PooledExecutor implements the Executor interface, so that's what we'll look at for the rest of this paper.
How does Executor work? The idea is, you're abstracting the idea of thread execution. With Java as it exists now, you can only run a Runnable by feeding it to a Thread, as in
Runnable r = new Runnable() {
public void run() {
// run something
}
};
Thread t = new Thread(r);
But with classes that implement the Executor interface, you can feed the Runnable to other mechanisms. You can run a Runnable object by
* creating a QueuedExecutor object, passing it a util.concurrent.Queue object in the constructor,
then executing the runnable with the QueuedObject's execute(Runnable r) method
(this gives you a queue and a single thread)
* creating a ThreadedExecutor object, passing the Runnable object in the constructor,
then executing the runnable with the ThreadedObject's execute() method (this gives you a single thread)
* creating a PooledExecutor object, passing it a Channel of some kind in the constructor,
then executing the runnable with the PooledObject's execute(Runnable r) method
(this gives you a queue of some kind and a thread pool whose size and characteristics you determine)
Make no mistake: it's Doug Lea's intention that you never use synchronize, wait or notify again it's also his intention that you never create a new Thread() and start() again.
In our example, we'll feed our thread to a thread pool. Threads in a thread pool can be in one of three states:
executing
queued
blocked
Now let's set up a PooledExecutor object with the policies that will accomplish our goals, then I'll explain how it works in the context of the example.
I create my thread pool so that it will run one thread at a time, and keep one thread queued to run. I do this by passing a BoundedBuffer one byte in size to the PooledExecutor constructor: this is where the thread queued to run will reside. We also pass a second parameter, an integer, to indicate the size of the active Thread pool: i.e., how many threads will be run concurrently by this thread pool? In this case, we want the pool to contain one thread.
So, if we pass the PooledExecutor two threads, one will run, and one will be queued to run. This is the code that does that:
pool = new PooledExecutor(new BoundedBuffer(1), 1);
pool.setMinimumPoolSize(1);
The call to setMinimumPoolSize() ensures that the thread pool live thread count will never drop below one; there will always be at least one live thread to run a Runnable object.
Now, what happens when the user starts feeding the thread pool even more threads, as the selector races up and down the JList? We need to set two more policies for the PooledExecutor, like this:
pool.waitWhenBlocked();
pool.discardOldestWhenBlocked();
Now, when another new thread is fed to the pool, and it can neither be executed nor queued, then it will be blocked; and as still more threads are fed, each new thread that hits the pool will cause the older thread, blocked, to be discarded.
Finally, I'll set my threads to stay alive for five seconds, and create a "warm" thread for the thread pool (which, remember, only contains one thread):
pool.setKeepAliveTime(1000 * 60 * 5);
pool.createThreads(1);
Now this doesn't quite give us our ideal behavior, but it works well enough. We will NEVER have more than three threads in this thread pool: one will be running, one will be blocked, and another will be queued to run. You can throw as many other threads as you want at the PooledExecutor object, but each new thread will simply cause the last blocked thread to be cycled out and discarded, and then the new thread itself will be blocked.
Once no new threads cycle out the blocked thread (after the cursor stops ), then, when the running thread completes, and after the queued thread runs, the blocked thread will itself run. Since it's the last thread, the GUI correctly displays the data for the item in the JList that the selector finally stopped on.
SUMMARY
Clients that issue remote calls for data over the Internet can flood a remote server with requests. It's important for the programmer to write his application so that widgets are smart enough to distinguish traversal of a list from an actual request for data. PooledExecutor in the util.concurrent library enables the application to control the number of remote data requests made in an intelligent manner.
10:40:50 AM
|