Peter Firmstone wrote:
Patricia Shanahan wrote:
Now that I've made some progress on building and testing, I'm ready to
return to the substantive issues of the TaskManager refactoring project.
I've read the "com.sun.jini.thread lock contention" thread. I think at
this point the best strategy is to keep TaskManager, update its
interfaces to improve performance, and use API classes available in
JDK 1.5 as applicable in its implementation.
I see two objections to directly exposing ThreadPoolExecutor or
similar to the TaskManager callers.
1. There are new features in 1.6 that may be useful, but not until
River moves to 1.6. I would prefer to avoid having to change all the
callers to make use of future features. For example RunnableFuture is
a 1.6 interface.
Even within a ThreadPoolExecutor based implementation, I think the
pending tasks can be handled most simply by not handing them over to
the executor until they are ready to execute.
Ok, that all makes sense.
2. River may need some global data collection and operations to
support resistance to denial of service attacks. Having a central task
manager class may make that easier, if it is ever needed.
A good idea.
I have two immediate questions:
1. Are the current tests considered adequate? If nobody has an
opinion, I'll review them before starting on refactoring.
I'm unsure to be honest. I don't have access to hardware needed to test
scalability.
Actually, in this question I was really trying to get at tests for
functional correctness. I regard good functional correctness tests as a
necessary prior condition for refactoring.
I can find out either by reading test code or by experiment, making some
deliberate mistakes and seeing whether the tests detect them. For
example, if I make TaskManager occasionally ignore a dependency will the
tests catch me doing it? If not, I need to write some tests that will
catch that.
2. What areas of TaskManager performance most need improvement? What
are my objectives? The current implementation seems to me to be likely
to work quite fast for lightly loaded instances with few tasks, and
even fewer pending tasks, but unlikely to scale well. Is that correct?
Are there any known test cases that demonstrate performance problems?
Gregg Wonderly reported the original bottleneck, caused by TaskThread
extending Thread. Gregg uses his own fork of Jini / Apache River for
delayed unmarshalling of proxy's, I'm not sure if he'd be able to run
the tests in his environment.
Another bottle neck was SecurityManager and AccessController security
checks, due to single thread synchronization of the original
net.jini.security.policy.DynamicPolicyProvider.implies(ProtectionDomain,
Permission), that was until now, it has since changed to an empty SPI,
where the administrator has the choice of using
ConcurrentDynamicPolicyProvider or DynamicPolicyProviderImpl which was
renamed from the old DynamicPolicyProvider implementation. Sorry for
the convoluted explanation.
Since TaskManager may queue Task's unbounded, as the queue grows, the
time window required for single thread synchronization grows, to check
that no other Task in that list should run first and if true, returning
the Task to the back of the queue, all while holding the lock. During
this time no TaskThreads can take another Task and no Task's can be
placed in the queue.
I guess a better solution might be to only lock the queue long enough to
take a private snapshot for the Task, but then this isn't memory
efficient for large queues. Perhaps what's needed is something to
delegate the priority decision to, so that locking the queue is limited
to adding and removing tasks, then we can use a ConcurrentLinkedQueue.
Perhaps:?
1. When given to the TaskManager, tasks are registered with a
TaskProrityManager.
2. Take Task from queue
3. Ask the priority manager if the task is ready.
4. If not ready, return to the back of the queue.
5. If ready, Execute.
6. Execution complete, notify TaskPriorityManager.
Tasks could belong to groups, so the tasks in that group are Comparable,
the Priority Manger could group the tasks with a ConcurrentHashMap,
reducing the number of tasks being compared, a group could just be a
string name, or an integer hash, defined by the task implementation.
The Priority Manager could add tasks to each group when en queued and
remove them when complete.
ConcurrentHashMap<Group,SortedMap<Task>> taskGroups;
interface Group {
String toString();
boolean equals(Object o);
int hashCode();
}
interface Task {
Group getGroup();
+ existing method & comparable.
}
Just some thoughts, based on your comments.
Here's another idea I've been playing with in my mind.
1. Replace the Task runAfter method with:
/** Find the first Task in an Iterable's iteration order on which this
Task depends. Return null if there is no such task.
**/
public Task getDependency(Iterable<Task> candidates);
There would be two typical implementations. If implementing class has no
dependency issues, it would unconditionally return null. If there are
dependencies, it would be:
public Task getDependency(Iterable<Task> candidates){
for(Task candidate : candidates){
if( some condition involving this and candidate ){
return candidate;
}
}
return null;
}
I'm thinking of keeping all the Task instances for which the TaskManager
is currently responsible in a TreeSet based on reverse arrival order.
The Iterable would always be set up to search in increasing age order,
so getDependency will return the youngest Task on which this depends.
Incidentally, the business with passing a size parameter along with the
List in the current implementation is unnecessary. All the Collection
classes come with good sublist or subset capabilities. ArrayList, in
particular, can return a List view of any index range.
Consider an add of a Task x. Use x.getDependency on the whole TreeSet.
If it returns a hit y, put x in a Set associated with y. We then don't
have to consider x for placement in the ready set until y is removed or
terminates.
When y is removed or terminates, search only tasks older than y to look
for another dependency. There is a good chance there won't be many at
that point. If x and y belong to a serial order subset, there won't be
any, because y would not have run until all older tasks had finished.
Essentially, this is the usual operating system strategy of putting
threads that are not currently runnable in a data structure associated
with the reason for non-runnability, outside the priority structure that
dispatches runnable threads. This minimizes the cost to the dispatcher
of a thread that cannot do anything useful until some disk read finishes.
This approach has its highest cost if the TaskManager has a lot of
tasks, and we are adding a Task depends on none of them but is of a type
that might depends on another Task, so it has to scan the entire Iterable.
TreeSet seems attractive because it has a good balance of fast append,
scan forwards or backwards from a specified element, and removal, but I
am still thinking about data structures, and may experiment with
alternatives.
What do you think about this idea?
Patricia