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.







Reply via email to