Processing framework review
The processing framework is a central subsystem of deegree 3. It is urgent to get this component into a stable state, because it is required for the WPS, which has to be delivered (as a prototype) at the end of September 2008. External developers will start to develop WPS processes then -- any later changes to the processing API will require changes on the side of the external code when they update to a later deegree 3 version.
This page reviews the current status of the processing framework and points out issues that need to be resolved/considered before the subsystem can be promoted to beta status.
1. Requirements
The WPS specification requires the following processing functionality:
- Synchronous and asynchronous execution of processes
- Monitoring of processes, i.e. status (paused, running, finished); progress information (percentage) is an additional "nice-to-have" feature
Another requirement -- which is not strictly a requirement of the WPS specification, but an external one -- is the ability to persist the list of processes (and the processes themselves). In the WPS implementation, this allows to guarantee that a started asynchronous process gets executed, even when the system is interrupted (e.g. Tomcat crashes). Execution of the process will restart when the WPS is up again.
The processing API should also provide the functionality of the deegree 2 concurrent API, i.e:
- Perform an arbitrary task asynchronously (in an independent thread) optionally with a maximum execution time.
- Perform an arbitrary task synchronously with a maximum execution time.
- Perform several task synchronously (but in parallel threads) optionally with a maximum execution time.
1.1. Shortcomings/missing features of the deegree 2 concurrent framework
Compared with the deegree 2 concurrent API, the processing API must meet the following additional requirements:
- Delayed execution of processes, repeating of processes
- Persisting of processes
- Process status handling including pausing, resuming, cancelling
- Fine grained progress information (eg. percentages)
2. Review of the current design
2.1. CommandProcessor
The CommandProcessor is responsible for the execution of commands (BTW, is there a difference between a process and a command? We should use a clear naming convention to avoid confusion!).
There are two extensions of the basic CommandProcessor interface (SynchronousCommandProcessor and AsynchronousCommandProcessor), but they do not define any additional methods. In both cases the execution methods are #execute( Command command, long timeout ) and #execute( Command command, long timeout, Calendar executionStart, int repeat, long interval ). It might be clearer to use different method names (#executeSynchronously(...) / #executeAsynchronously(...). This would also reduce the number of interfaces / classes in the subsystem -- a single CommandProcessor interface should be sufficient then.
The implementation of the QuartzSyncCommandProcessor delegates the Command execution to it's own thread and uses a polling loop to check if the command finished executing. This is generally discouraged, because it always results in a CPU load/lag tradeoff: if the polling interval is short, the CPU load for the polling loop is high. If the polling interval is long, a lag occurs until the thread notices that the execution has finished. Thus, the delivery of a result takes longer than necessary. A better way to do this is using Future-objects (Java API) or semaphores (low-level signalling).
- Is there a use-case for persisting or scheduling synchronous commands? It should be noted that it introduces overhead to use Quartz (or any other "enterprise-level scheduler") for the execution of synchronous commands. If the features are not required, the Quartz framework should not be used. Quartz is not designed for the execution of synchronous commands.
Is there a use case for registering listeners in a SynchronousCommandProcessor? The listener concept is inevitable for communicating the result of an asynchronous computation; but for synchronous execution it's generally more convenient to retrieve a result as a return value.
2.2. Command
The Command interface must be implemented to define a method that can be executed by a CommandProcessor instance.
The PersistableCommand interface is empty. Why is it needed then? The same goes for other empty interfaces.
- Undo/redo is not something that is possible or makes sense for all use-cases of the processing API. It would probably be better to handle it in a separate API layer.
What about ExecutionPlan and CommandGroup? Why not just use a list of commands?
There is basically no exception handling. If a command throws an exception, the exception is logged, and wrapped in a CommandProcessorException, which is a RuntimeException. Important exceptions will get lost if one is not careful to extract the wrapped exception by hand. It would be better, if the API client is forced to cope with exceptions that are thrown during process execution (as in the deegree 2 concurrent API).
The Command API forces the command writer to care about many things that could be handled automatically by the CommandProcessor. For example, the CommandProcessor tries to execute a command, and it fails with an exception. So the CommandProcessor knows this and could propagate the exception. The same is valid for some of the other STATE values, for example started, queued and arguably also cancelled, paused etc.. also some of the methods of CommandState. It also seems intended that the Command cares about notifying ProcessMonitors itself, and setting its own priority. It would be better if the monitor was outside the monitored object. The priority handling should be part of the scheduler, not the scheduled process.
2.3. General design notes/questions
Generics should be used consistently. The Command interface has the processes result type as type parameter, but for example in AbstractCommand and the CommandProcessor interface it is not used anymore.
- A generic processing API should be able to at least provide a mechanism to use Callable and/or Runnable instances, because these are the standard java abstractions for processes.
- The usage of Identifier/Identifiable does not seem to net any benefit in the processing framework. The Java reference to a Command object is by definition unique. If the actual process object needs to be hidden, a handle object could be returned upon process execution, and the implementation could ensure uniqueness of the handle (and need not trust the API user).
2.4. Bugs found during evaluation
Listeners can be set for a CommandProcessor and for a job. Setting it for a job has no effect, though.
- Using the command processors with a command that does not return an identifier results in a null pointer exception.
When implementing just the Command interface, Quartz complained about not being able to notify a job listener, and threw a null pointer. Extending AbstractCommand instead, works around this problem.
- Synchronous execution does not respect the timeout. Execution is always carried out in full, no matter which timeout is given (tested with -1, 2, 2000).
3. Writing processes for the WPS
To write your own process, you'll have to implement the Process interface. This currently involves a createCommand method to create the Command to execute from the WPS execute request, and a getCommandProcessListener method to retrieve a listener to be added to the command.
So you'll also need to have a custom Command that actually executes the process, since the Process interface is designed to deal with WPS request specifics/data input etc. Here we have the WPSCommand interface, that deals with WPS like result generation (getXMLResult and writeResultToFile), and the Command interface respectively the AbstractCommand class from the processing package.
The AbstractCommand class forces you to implement methods such as cancel, pause, resume, getResult. For WPS purposes, these methods may not always be necessary (for example, WPS does not have a method of cancelling requests). The getResult method forces you to implement the CommandResult interface, just to transport the result of your operation.
Concluding, a better separation between the actual process and the WPS specific input/output would be desirable. For the WPS specific input/output requirements, a toolbox should be created, which enables you to quickly generate result documents etc. For a lean implementation of a WPS process, the Command interface seems to have far too many capabilities. If one wants to define the example process as a Callable (Standard Java abstraction for a "command" with a result), it may look like this:
public class AdditionCallable implements Callable<Integer> {
private int a, b;
public AdditionCallable( int a, int b ) {
this.a = a;
this.b = b;
}
@Override
public Integer call()
throws Exception {
return a + b;
}
}
The same code using the Command interface looks like this:
public class AdditionCommand implements Command<Integer> {
private int a, b;
int c;
public AdditionCommand( int a, int b ) {
this.a = a;
this.b = b;
}
@Override
public void addCommandProcessorListener( CommandProcessorListener listener ) {
// what to do here?
}
@Override
public void cancel() {
// or here?
}
@Override
public void execute() {
c = a + b;
}
@Override
public Identifier getIdentifier() {
// or here?
return null;
}
@Override
public User getOwner() {
// or here?
return null;
}
@Override
public CommandResult<Integer> getResult() {
return new CommandResult<Integer>() {
@Override
public CommandState getState() {
// or here?
return null;
}
@Override
public Integer getValue() {
return c;
}
};
}
@Override
public void pause() {
// or here?
}
@Override
public void resume() {
// or here?
}
@Override
public void setPriority( int priority ) {
// or here?
}
@Override
public void setProcessMonitor( ProcessMonitor processMonitor ) {
// or here?
}
}
4. Practical tests
We set up a small test case computing the md5 hash from a randomly filled byte array. Besides an implementation for the processing framework we also implemented a Callable and executed it through the java concurrency framework (upon which the deegree 2 framework is built). The execution times were measured (note, that the initialization of the Quartz-Framework falsifies the timing-results). One typical run's output looks like this:
4.1. Normal run
#### Callable Test ##### e804c4a71f976aebba917f7d84426fe4 Callable: 5327ms #### Current processing framework (Quartz) #### [09:52:53] INFO: [QuartzScheduler] Quartz Scheduler v.1.6.0 created. [09:52:53] INFO: [RAMJobStore] RAMJobStore initialized. [09:52:53] INFO: [StdSchedulerFactory] Quartz scheduler 'Sched1' initialized from default resource file in Quartz package: 'quartz.properties' [09:52:53] INFO: [StdSchedulerFactory] Quartz scheduler version: 1.6.0 [09:52:53] INFO: [QuartzScheduler] Scheduler Sched1_$_1 started. name:0 name:0 name:0 e804c4a71f976aebba917f7d84426fe4 Command: 6516ms
This means that the callable runs in about 5.3 seconds, while the command processing framework (using synchronous execution) runs in about 6.5 seconds. This includes starting up Quartz and the processing framework, which probably explains the difference.
The code I used can be found here: attachment:TestCallable.java, attachment:TestCommand.java, attachment:ProcessingTester.java
4.2. Failing run
Next we tested a similar, but slightly modified version that throws an exception during execution.
#### Callable Test ##### java.security.NoSuchAlgorithmException: md6 MessageDigest not available Callable: 144ms #### Current processing framework (Quartz) #### [10:05:27] INFO: [QuartzScheduler] Quartz Scheduler v.1.6.0 created. [10:05:27] INFO: [RAMJobStore] RAMJobStore initialized. [10:05:27] INFO: [StdSchedulerFactory] Quartz scheduler 'Sched1' initialized from default resource file in Quartz package: 'quartz.properties' [10:05:27] INFO: [StdSchedulerFactory] Quartz scheduler version: 1.6.0 [10:05:27] INFO: [QuartzScheduler] Scheduler Sched1_$_1 started. name:0 name:0 name:0 java.security.NoSuchAlgorithmException: md6 MessageDigest not available Command: 535ms
The time is as expected again slower for the processing framework, probably again due to the startup of Quartz and the framework. What's interesting to see is that for the processing framework, one has to code a lot by oneself. That includes providing a way for the executing code to see the actual exception BY HAND! The Java concurrency framework just throws an exception, where one can extract the original cause easily. The deegree 2 concurrent framework just throws the original exception upon value retrieval, which is most convenient.
Here's the code for the second test case: attachment:TestCallable2.java, attachment:TestCommand2.java, attachment:ProcessingTester2.java
5. Conclusion
It doesn't seem feasible to promote the processing subsystem to beta state just now.
5.1. Issues
The most important issues that must be reconsidered or need to be taken care:
The CommandProcessor hierarchy appears unnecessary complex.
- The implementation of a Command is far too complex for most common cases. If we want the processing API to be used for general concurrency issues in deegree3, we need to reduce the amount of necessary implementation code for the execution of a concurrent task (Command). Also, if we care about the GDI-grid project, we cannot really expect WPS process developers to implement lots of unnecessary methods that aren't used.
- If used as a general solution for the execution of concurrent tasks in deegree3, the overhead of delegating this to Quartz could be problematic (after all Quartz is not a concurrency framework, but an "enterprise-level scheduler"). Also the option to execute a number of Commands in separate threads synchronously (i.e. get the results of all Commands after execution) is missing. A use case for this is the generation of several layers in the WMS: it is very convenient to invoke the drawing of all layers. When all results are available, the thread is awaken and can generate the resulting overlay.
- There is no real propagation of Exceptions that appear during Command execution. The API client should be forced to cope with exceptions (a better way to do this is implemented in the deegree2 concurrent framework).
- The usage of polling is generally discouraged (as explained above).
- Some (concurrency) aspects have a cleaner solution in the deegree2 concurrent framework. Of course, the new processing framework offers a lot more functionality, but deegree3 subsystems should not introduce any deteriorations when compared with their corresponding deegree2 components.
- As the processing framework is such a crucial component, a thorough documentation of its design is needed.
5.2. Thoughts
The mentioned issues need to be resolved quickly, because the processing API needs to be (mostly) stable for the rollout of the WPS prototype at the end of September 2008.
- The current processing API seems to encapsulate only a small subset of the power of Quartz, and it appears unlikely that any other framework could be used as a drop-in replacement for it anytime soon. Maybe Quartz should not be hidden behind an API and rather be used directly for tasks that require it.
- The deegree2 concurrency API could be enriched by a few new features and be used for tasks where complex scheduling and persistency is not required.
- Idea: Use a Callable/Runnable as standard implementation for concurrent tasks. If advanced Command features are required, use the Processing Framework to create a full-blown Process object that is handled by Quartz.