So it would seem that the runAfter() method is superfluous and
unnecessary in most, if not all cases? Especially if the caller is
making the decision about when tasks are placed on the queue.
We do need to confirm that in our current implementations, the callers
are taking responsibility for the queue placement. Then try to
discovery why we needed to have a dependency check by the TaskManager.
It sounds like for absolute concurrency the design needs to be totally
revised, perhaps the responsibility of determining if the Task is ready
to be executed should be placed solely on the Task implementation
itself, but not by passing in the list of all Tasks on the queue, but
with a simple:
interface Task extends Runnable {
boolean ready();
}
Or perhaps it should be an Event like system. The Task could be
notified by all the immediate Tasks it depends on (direct dependency
links, not an entire chain) when they have completed. When the Task
receives all completion notifications for required dependencies, it
places itself on the TaskManager queue, when it completes it notifies
all it's dependants.
Tasks could register dependencies with each other (as could anything
that had a reference to a Task), then if Tasks are arriving from remote
machines, and they have dependency links, prior to being serialized,
then deserialization will assist with sequencing.
The incumbent TaskManager could be retrieved from a static factory method.
interface Task extends Runnable {
void runFirst(Task t) throws CircularReferenceException;
void runAfter(Task t) throws CircularReferenceException; // If
complete notify completion should be called back immediately.
void notifyCompletion(Task t);
void poke(); // Responsible for placing Task on queue if not done so
already and only when it has no dependencies. Must be called at least once.
}
When a Task is notified that some other Task must be run first, it
notifies the Task it receives, that it must run after it. A task keeps
an internal Set of dependencies, every time a notifyCompletion is
recieved, it removes that Task from the Set and every time it receives a
runFirst, it adds a Task, once the Set isEmpty(), the Task places itself
on the TaskManager queue. A Task also keeps a set of dependant's to
notify completion. This looks after Garbage Collection and the interface
is idempotent. This also might mean that some Tasks are executed on
more than one machine, but that wouldn't matter, the Task is guaranteed
to execute at least once, after poke() has been called. The poke()
method might call the poke() method on all it's dependencies (but not
dependants) to guarantee execution.
You would only need to know one Task in the tree to cause execution to
proceed. Any Implementation coordinating the Tasks only need to know
the other Tasks that immediately proceed it.
References look after the rest. I wonder if it would need a stop
button, it would be sort of like a chain reaction, maybe a cancel()
method, which would call cancel() on all dependants, causing all run()
methods to return immediately.
Just a thought.
Cheers,
Peter.
Gregg Wonderly wrote:
As you note below, a sequence number or some other ordering mechanism
is the most common way that is used to order tasks. The task objects
are added to the queue in the order that the sequence number
increases, so it's not possible for an task to appear in the queue
until after its dependents (those that execute before it) are already
there.
This type of logic is essential to making this type of thing work.
Gregg
You know, task implementation objects could communicate using a
static atomic integer sequence id, all task objects could use it to
determine if they are ready to execute, if a task has an object
sequence number, equal to the sequence id, then it is ready to
execute, if less, it could throw a sequence exception, if greater, it
isn't ready. The same behaviour could be managed by a Group
implementation.
Just an idea, don't know if it is relevant to any implementations.
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.