Wednesday, March 25, 2009

Compiled Configurable TOAST

In between completing fully functional crash recovery for the Web Services Transactions (XTS) code and writing a comprehensive test suite for it I've been able to squeeze in a couple of nice extensions to the functionality of TOAST, the code injection tool I have been using to automate XTS crash recovery testing.

Attentive readers may recall that TOAST is a nifty, easy to use tool for injecting side-effects into Java code. I have been using TOAST to test XTS recovery by injecting code which crashes the JVM in the middle of a transaction commit. I also use TOAST to inject further faults during recovery in order to delay or drop messages exchanged between the transaction coordinator and the web service participants it is trying to corral into a successful commit. Finally, I inject trace statements at suitable locations in the code which track the progress of the test and allow the test client to identify successful or failed execution.

None of this requires any modifications to or recompilation of the deliverable application being tested. TOAST just needs to be supplied with a script identifying the trigger points, locations in the application code which need to be tweaked, and the Java expressions which are to be (conditionally) executed at those trigger points. Point your JVM at the TOAST jar and the script when you start your Java application and TOAST makes sure all the required side effects get executed.

For those who have not yet had a look at TOAST I'll recap a little. Scripts comprise a sequence of Event Condition Action (ECA) rules. The Event part is the specification of the trigger point, a class name, a method located in that class and some location in the method code. Locations identify a point in the source code through reference to the code structure. Examples might be: the start of the method (AT ENTRY) when a return occurs (AT EXIT), before the first read of an instance field (AT READ myField), after the 2nd call to a method (AFTER CALL getMyField() 2) and so on. The event specifies WHEN the side-effects coded in the rule are are considered for execution by describing a trigger point. Whenever a thread reaches this trigger point it enters the rule engine to decide whether to fire the associated rule.

The Condition part is a Java expression specifying WHETHER to fire the rule. If this evaluates to true then the rule fires and executes its Action. If the condition is false then control returns to the trigger method and execution continues as normal.

The Action part specifies WHAT side-effects need to occur. It is a sequence of Java expressions to be evaluated one after another. The last expression may be a return or a throw action. These cause an immediate abnormal return of control from the triggering method bypassing normal execution of the rest of the method code. If the method is non-void then the return action must include an expression evaluated to compute the return value. A throw action must include an exception type and arguments for the exception constructor.

In order to simplify testing of multi-threaded code TOAST's rule language also includes built-in methods for use in conditions and actions. These are a variety of operations which simplify the development of multi-threaded tests. For example, one rule might include a call to waitFor() in its action list, suspending any thread which fires the rule. Another rule might include call to signalWake() in its action. If a second thread fires this rule then the call to signalWake() wakes up the suspended thread. This can be used to ensure that the first thread is only allowed to progress once the second thread has reached a suitable point during its execution.

So, what are the two updates which I have sneaked in? Well, the first one is to compile the rule code to bytecode. Up until now the rule execution engine has worked by interpreting the rule parse tree. Method and builtin calls were executed using reflection. Java operations were executed by evaluating their arguments and then combining them based on the operation type. Well, the latest version of TOAST now compiles the rule code into Java bytecode attached to a helper class associated with the rule. This bytecode is handed to the JIT compiler which means it can be compiled to native machine code and optimized like any other Java method. This will not make a lot of difference for rules which are only executed once or twice but it will make a big difference to the usability of TOAST in cases where rules are triggered many times.

The second update is a very nice generalization of the built-in functionality. The rule parser, type-checker and compiler don't actually care much about the presence of builtin methods. As far as they are concerned a call is a call is a call. The type checker, in particular, attempts to resolve calls by looking at the method name and signature i.e. the types of the call arguments. Any calls whose name and signature matches a public instance method of a class called Helper are treated as built-in calls. When the rule is triggered an instance of Helper is created to store the list of event bindings. A built-in call can be executed by calling the matching method with this instance as the method target. The compiler actually generates a subclass of Helper whose execute() method comprises the compiled rule bytecode. So, built-in calls are actually just invocations of methods on this.

Now, this mechanism is completely independent of the details of how class Helper is defined. Any public POJO class could actually be used to define the rule language built-ins. So, a simple declaration in the rule source specifying the name of an alternative class will allow the rules to use a different helper more appropriate to the application in question. A domain-specific helper class makes scripting of side-effects simpler and tidier by encoding complex, application-specific actions as single operations. If you like you can keep the current built-ins and add some more of your own by extending class Helper and adding extra public methods. If you don't like how one of the default built-in operations works you can override the version Helper provides to re-implement it.

These upgrades are described more fully in the TOAST documentation. Please download TOAST, take a look and give it a test run.

No comments: