Sunday, September 11, 2011

Asynchronous Method calls with Groovy: @Async AST

At work, I needed to create a very simple background job, without any concern about what I could get back, because mostly all the hard work was just batch processing and persistence, and all exceptions or roll-back concerns were already taking care of.

At the beginning I used a very simple way to call my background job, using: Executors.newSingleThreadExecutor()

 void myBackgroundJob() {
    Executors.newSingleThreadExecutor().submit(new Runnable() {               
                        public void run() {
                        //My Background Job
                    }
                });
  }

And it worked great, just what I needed.

Using Groovy facilitate even more the way to create a new Background job, as simple as:

def myBackgroundJob() {
    Thread.start {
        //My Background Job
    }
}

Then, after this simple way to send something into the background, I decided to create a new AST in groovy, that remove the need to remember or copy and paste the same logic.

I created two annotations that help to identify the class and the methods that are going to be put into a new Thread.

One for the Class:

package async

import org.codehaus.groovy.transform.GroovyASTTransformationClass
import java.lang.annotation.*
import xml.ToXmlTransformation


@Retention (RetentionPolicy.SOURCE)
@Target ([ElementType.TYPE])
@GroovyASTTransformationClass (["async.AsyncTransformation"])
public @interface Asynchronous { }

And the other for the Method:


package async

import org.codehaus.groovy.transform.GroovyASTTransformationClass
import java.lang.annotation.*
import async.AsyncTransformation

@Retention (RetentionPolicy.SOURCE)
@Target ([ElementType.METHOD])
@GroovyASTTransformationClass (["async.AsyncTransformation"])
public @interface Async { }


then the Asynchronous Transformation, using the AstBuilder().buildFromString(). Here I combined a GroovyInterceptable to connect the method being call with the AST transformation to wrapped it with the Thread logic.




package async

import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.transform.*
import org.codehaus.groovy.ast.*
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.ast.builder.AstBuilder

import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.ClosureExpression
import org.codehaus.groovy.ast.expr.ConstantExpression

import org.codehaus.groovy.ast.stmt.BlockStatement

import org.codehaus.groovy.ast.expr.ClassExpression
import org.codehaus.groovy.ast.expr.ArgumentListExpression

@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
class AsyncTransformation implements ASTTransformation{

    void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        if (!astNodes ) return
        if (!astNodes[0] || !astNodes[1]) return
        if (!(astNodes[0] instanceof AnnotationNode)) return
        if (astNodes[0].classNode?.name != Asynchronous.class.name) return

        def methods = makeMethods(astNodes[1])
        if(methods){
            astNodes[1]?.interfaces = [  ClassHelper.make(GroovyInterceptable, false), ] as ClassNode []
            astNodes[1]?.addMethod(methods?.find { it.name == 'invokeMethod' })
        }
    }

    def makeMethods(ClassNode source){
         def methods = source.methods
         def annotatedMethods = methods.findAll {  it?.annotations?.findAll { it?.classNode?.name == Async.class.name } }

         if(annotatedMethods){
             def expression = annotatedMethods.collect { "name == \"${it.name}\"" }.join(" || ")
             def ast = new AstBuilder().buildFromString(CompilePhase.INSTRUCTION_SELECTION, false, """
                package ${source.packageName}

                class ${source.nameWithoutPackage} implements GroovyInterceptable {

                    def invokeMethod(String name, Object args){

                        if(${expression}){
                            Thread.start{
                                def calledMethod = ${source.nameWithoutPackage}.metaClass.getMetaMethod(name, args)
                                calledMethod?.invoke(this, args)
                            }
                        }else{
                           def calledMethod = ${source.nameWithoutPackage}.metaClass.getMetaMethod(name, args)?.invoke(this,args)
                        }
                    }

                }
             """)

             ast[1].methods
         }
    }

}



The example:

package async

@Asynchronous
class Sample{

    String name
    String phone

    @Async
    def expensiveMethod(){
        println "[${Thread.currentThread()}] Started expensiveMethod"
        sleep 15000
        println "[${Thread.currentThread()}] Finished expensiveMethod..."
    }

    @Async
    def otherMethod(){
        println "[${Thread.currentThread()}] Started otherMethod"
        sleep 5000
        println "[${Thread.currentThread()}] Finished otherMethod"
    }
}

println "[${Thread.currentThread()}] Start"
def sample = new Sample(name:"AST Sample",phone:"1800-GROOVY")
sample.expensiveMethod()
sample.otherMethod()
println "[${Thread.currentThread()}] Finished"


Final Notes:

As you can see on the example  I need to have the Asynchronous annotation on the class still. It could be better without it and just annotate the methods, something like the Groovy's SynchronizedASTTransformation. If you have any idea to complement this small example, please clone the source code [here], and let me know what you think.

I could used the @javax.ejb.Asynchronous or the Spring's @org.springframework.scheduling.annotation.Async, but I only needed a very simple solution without any other configuration or library inclusion.


The remain logic here could be play more with multi threading and expect some results like: java.util.concurrent.Future and its java.util.concurrent.Future.get() method or maybe integrated with another frameworks like Spring.

Source Code: [here]

1 comment:

  1. Good stuff. There's no need for two annotations though - you can specify the target as being class or method:

    @Target([ElementType.METHOD, ElementType.TYPE])

    ReplyDelete