Pineapple defines an framework for monitoring and controlling the execution of operations. The purpose of the framework is to:
The core class in the framework is the ExecutionResultImpl class which implements the ExecutionResult interface. The class is used by Pineapple to capture runtime information about and control the execution of operations.
A new execution can be create in three different ways:
To start a new execution which should tracked by execution results, create an instance by invoked the new operator:
ExecutionResult rootResult = new ExecutionResultImpl("some description");
The semantics of invoking the constructor ExecutionResultImpl( String description ) is:
To start a new execution which should tracked by execution results, invoke the startNewExecution(String description) on a result repository to create a root result:
ExecutionResult rootResult = resultRepository.startNewExecution( "some description" );
The semantics of the startNewExecution( description ) is:
Branching of the execution path can be tracked by adding a child result to an existing execution result:
ExecutionResult modelResult = rootResult.addChild( "some description" );
The semantics of the addChild( description ) method is:
TODO: clarify what happens if a child is added to a non-running result?
A execution result contains an state variable which defines the runtime state of the application whose execution is documented by the result.
The legal states are:
The legal states is defined as an enum named com.alpha.pineapple.execution.ExecutionResult.ExecutionState which is used to set and query about the states of execution results when using the framework:
public interface ExecutionResult { /** * Defines the execution state of a execution result. */ enum ExecutionState { COMPUTED, // completed execution state is computed from children SUCCESS, // completed execution, execution was successful FAILURE, // completed execution, execution resulted in failure ERROR, // completed execution, execution encountered an unexpected error EXECUTING, // execution is in progress INTERRUPTED // completed execution, execution was interrupted (either cancelled of aborted due to failure) } // remaining interface definition here.. }
When the execution (or the application part which is tracked by an execution result) is complete the execution result needs to be updated to reflect the outcome of the execution. Set the state using the methods:
..which add one or more messages using a message source and sets the state. Usage of the different methods are described in the following sections.
Finally the state can be set using the method setState(...), but using this method has the disadvantage that messages needs to be added afterwards
The success state is used to capture the successful execution of some code. An example could the successful execution of an test command where the assertion of the expected and actual values succeeded. An example of a test command which uses the success and failure state to report the outcome of the test can be found here: How-to: Write a simple test command whose outcome is reported using the execution framework.
The state of the execution result object is set using the completeAsSuccessful(..) method:
// assert if (ip.equalsIgnoreCase( actualIP )) { // set successful result Object[] args = { this.hostname, this.ip }; executionResult.completeAsSuccessful(messageSource, "trnc.succeed", args); return; } // else set failed result
The semantics of the completeAsSuccessful(MessageSource messageSource, String key, Object[] args) is:
The success state is used to capture the unsuccessful execution of some code. An example could the unsuccessful execution of an test command where the assertion of the expected and actual values failed. An example of a test command which uses the success and failure state to report the outcome of the test can be found here: How-to: Write a simple test command whose outcome is reported using the execution framework.
The state of the execution result object is set using the completeAsFailure(..) method:
// assert if (ip.equalsIgnoreCase( actualIP )) { // set successful result return; } // set failed result Object[] args = { this.hostname, this.ip.toString(), this.actualIP.toString() }; executionResult.completeAsFailure(messageSource, "trnc.failed", args);
The semantics of the completeAsFailure(MessageSource messageSource, String key, Object[] args) is:
The error state is used to capture unexpected exceptions in logic/code whose execution is documented by execution results. An example of a test command which uses the error state to report errors during execution of the test can be found here: How-to: Write a simple test command whose outcome is reported using the execution framework.
If exceptions is caught by a try/catch block then the error state can set be using the completeAsError(..) method:
ExecutionResult myResult = ... // some logic try { // some logic // set result as successful, failed or compute it } catch (Exception e) { myResult.completeAsError(messageSource, "dmtd.error", null, e); }
The semantics of the completeAsError(MessageSource messageSource, String key, Object[] args, Exception e) is:
The execution can alos be completed with an error using an exception is input:
ExecutionResult myResult = ... // some logic try { // some logic // set result as successful, failed or compute it } catch (Exception e) { myResult.completeAsError(e); }
The semantics of the completeAsError(Exception e) is:
Logic whose state should be computed based on the results of its child results should implement it this way:
Object[] args = { type }; myResult.completeAsComputed(messageSource, "dmtd.completed", args);
The semantics of the completeAsComputed(MessageSource messageSource, String key, Object[] args) is:
There is an more advanced way to set the state as computed which will produce different messages depending on the whether computed state is
The advanced method is invoked this way:
Object[] successfulArgs = { type }; Object[] failedArgs = { type2 }; myResult.completeAsComputed(messageSource, "xyz.completed", successfulArgs, "xyz.failed", failedArgs);
The semantics of the completeAsComputed(MessageSource messageSource, String successfulKey, Object[] successfulArgs, String failedKey, Object[] failedArgs) is:
Using the advance version requires that the failed message is defined with at least two arguments:
tdc.failed=XYZ failed, because [{0}] child executions failed and [{1}] child executions terminated with an error.
It is possible to invoke the method with null argument lists:
myResult.completeAsComputed(messageSource, "xyz.completed", null, "xyz.failed", null );
..but the requirement for the failed message is the same.
Input to the state computation:
The algorithm is:
Comments regarding the input state and the algorithm:
..when state is set then parent is inspected..
.. if parent isn't running (i.e. have already be set) then parent state is updated by setting the parent state computed to trigger a recomputation of the parent state..
..starts when execution result is created
.. stops when state is changed from initial running state to something else
..cannot be restarted.
..messages can be stored used the addMessage method
..default names:
The default names are defined as constants on the ExecutionResult interface:
public interface ExecutionResult { public final String MSG_MESSAGE = "Message"; public final String MSG_ERROR_MESSAGE = "Error Message"; public final String MSG_STACKTRACE = "Stack Trace"; public final String MSG_COMPOSITE = "Composite Execution Result"; // remaining interface definition goes here.. }
Example of storing a message using one of the constants:
....
A result is created with a continuation policy which defines how the execution should react to events regarding the interruption of the execution. The continuation policy supports these event:
The continuation policy is created when a root result is created. The continuation is global/shared for the root result and all child results in an execution.
The continuation policy is created with the directive:
Pineapple sets this value based on the content of models. Pineapple uses the content of models to directs it runtime behavior to support the continuation policy.
The result continuation policy can be obtained from a result using the method:
ContinuationPolicy policy = result.getContinuationPolicy();
The directive can be modified:
ContinuationPolicy policy = result.getContinuationPolicy(); policy.disableContinueOnFailure();
or (to confirm the default value):
ContinuationPolicy policy = result.getContinuationPolicy(); policy.enableContinueOnFailure();
The API is designed in such a way that the directive can only be changed once.
If the continuation directive is disabled and an error, failure or interruption is registered by the framework then it will capture the "failed" result and throw an exception to signal that the execution should be aborted.
The prerequisite is to disable the continuation directive:
ContinuationPolicy policy = result.getContinuationPolicy(); policy.disableContinueOnFailure();
..then an execution (or part of it) is completed by setting the state of an result:
// create child result ExecutionResult childResult = rootResult.addChild( "some child description"); // complete childResult.completeAsError(messageSource, "dmtd.error", null, e);
The framework implements the algorithm to signal that execution should be aborted:
policy.setFailedResult(failedResult);
Users of the framework should take note when this exception is thrown and in a friendly cooperative approach:
To avoid the presence of InterruptedExecutionException the continuation policy can be queried as to whether the execution should be aborted or continue:
// query whether execution should continue if(!policy.continueExecution()) return; // continue execution
Interested parties can implement the ResultListener interface and then register them self as an observer at the core component.
The core component implements the observer pattern to provide a facility for clients to be notified in real time of how the execution of an operation proceeds.
The core component has the role of subject in the observer pattern. To support the role, it provides the addListener( ResultListener listener ) method which should be used by observers to register themselves. The core component contain logic which notifies all registered observers during execution of an operation.
Clients should implement objects with the role of observer and register them with the core component to be notified of how the execution an operation proceeds.
An observer must implement the com.alpha.pineapple.execution.ResultListener interface:
public interface ResultListener { void notify(ExecutionResultNotification notification); }
Each time an execution result object changes it state, all registered observers are notified of the change. The notification is implemented by invoking notify(ExecutionResultNotification notification) on the each registered observer.
The argument is a notification object which contains:
To capture the state when listeners are notified the state is recorded separately in the notification. Since the state of an execution result is mutable, the recorded state in the notification might not match the state of the execution result since its state can have changed since the creation of the notification and the notification of the observers.
An observer can query the the execution result about its new state and additional info.
The ExecutionResultFactory interface defines an interface for creation of execution results objects.
The Spring application context for the API project contains a bean named executionResultFactory which implements the ExecutionResultFactory interface. The bean is implemented by the DefaultExecutionResultFactoryImpl class.
Inject the result factory using Spring:
@Resource ExecutionResultFactory executionResultFactory;
A result instance can be created using the factory
ExecutionResult result = executionResultFactory.startExecution("Some description");
The semantics of the startExecution(String description) is: