Skip to main content

A dozen things to know about AWS Simple Workflow in Eclipse and Maven

Amazon AWS Simple Workflow


AWS Simple Workflow(SWF) from Amazon is a unique workflow solution comparing to traditional workflow products such as JBPM and OSWorkflow. SWF is extremely scalable and engineer friendly(in that flow is defined with Java code) while it comes with limitations and lots of gotchas.

Always use Flow Framework


The very first thing to know is, it's almost impossible to build a SWF application correctly without Flow Framework. Even though the low level SWF RESTful service API is public and available in SDK, for most workflow with parallelism, timer or notification, consider all possibilities of how each event can interlace with another, it's beyond manageable to write correct code with low-level API to cover all use cases. For this matter SWF is quite unique comparing to other thin-client AWS technologies.

The SWF flow framework heavily depends on AspectJ for various purposes. If you are not familiar with AspectJ in Eclipse and Maven, this article talks about challenges you may encounter while working with Flow Framework in Eclipse and Maven based on AWS Java SDK 1.3.26, and helps you understand what's going on.

Understand what needs to be done


Before following the official tutorial, it's important to understand what needs to happen and what development environment should achieve. Here's a list of steps, some of which is done by engineer and some is done by development environment which is either Eclipse or Maven. Engineer should be able to write and run fully functioning workflow with Eclipse only or Maven only.

  1. Write Workflow interface that defines interaction point of workflow, including how workflow is started, what signal we want to send to affect running workflow execution and the read-only call to get state of execution. Write activity interface. This step is documented here in official tutorial.
  2. Have Eclipse or Maven do APT code generation for activity and workflow interfaces. Note that client code generation has nothing to do with load-time vs. compile-time weaving. Client code is always generated at compile time of workflow and activity interfaces. AspectJ weaving applies to the workflow implementation and generated activity client code. For a FooWorkflow interface, if AspectJ is configured property, 10 classes should be generated at compile-time. They are
    1. FooWorkflowCliet
    2. FooWorkflowClientImpl
    3. FooWorkflowClientFactory
    4. FooWorkflowClientFactoryImpl
    5. FooWorkflowClientExternal
    6. FooWorkflowClientExternalImpl
    7. FooWorkflowClientExternalFactory
    8. FooWorkflowClientExternalFactoryImpl
    9. FooWorkflowSelfClient
    10. FooWorkflowSelfClientImpl 
  3. For a FooActivities interface, 2 classes should be generated by AspectJ. Please verify if Eclipse or Maven correctly creates these generated java source and compile them to classes directory. If not, later part of this article will help troubleshoot.
    1. FooActivitiesClient
    2. FooActivitiesClientImpl
  4. Write implementation of Workflow interface using generated client code. The generated client code is used in Workflow implementation to call activity, run child workflow or restart itself, etc. Without correct client code this step can not be done.
  5. Now it comes to AspectJ weaving for Asynchronous and ExponentialRetry annotations. This step is to make sure workflow implementation is executed correctly at runtime by injecting additional code logic around the calls to methods annotated with two annotations. Normally it is the workflow implementation class and activity client code, FooActivityClientImpl.java, that calls annotated methods, so both workflow implementation and activity client code need AspectJ weaving. For this step user can choose between load-time weaving and compile-time weaving.
    1. For load-time weaving, workflow implementation is compiled like normal classes, the aop.xml file must exist in classpath and define the patterns of classes to apply advices and JVM must be launched with "-javaagent:/path/to/a/aspectjweaver.jar".
    2. For compile-time weaving, workflow implementation is compiled with AspectJ compiler and numerous inner classes are generated within workflow implementation and activity client code, around each call to annotated methods. There's no special requirement to JVM at runtime so the aop.xml file is not required.
  6. It's very important to write unit test for every workflow implementation. I found almost everyone, me included, makes mistake in the first version without unit test. Without weaving unit test can't run property, and it doesn't always fail either. It'd be hard to troubleshoot so it's important to verify previous step is done correctly before running unit tests. Following the tutorial, I also learned that the SWF junit classes does not work for junit 4.10 or newer version, 4.7 is the latest working version I tried.
  7. Launch WorkflowWorker and ActivityWorker with spring or not.



Eclipse

Client code generation


These steps are required in Ecilpse to enable APT code generation:

  • Workflow and activity interfaces are annotated with @Workflow and @Activities.
  • Annotation processing is enabled in Project properties/Java Compiler/Annotation Processing.
  • AWS Java SDK jar and build-tool jar are in classpath, this usually means com.amazonaws:aws-java-sdk and com.amazonaws:aws-java-sdk-flow-build-tools is in pom.xml

If all three steps are done code generation should happen. Java code should be created to .apt_generated. If .apt_generated is empty verify the steps above.


 

Compile time weaving


Eclipse does compile-time weaving when

  • Project is converted into an AspectJ project
  • AWS Java SDK java is in AspectJ build path

One way to verify if Eclipse is configured right to interpret annotation is to look for the markers on left side of source editor for each call to method with @Asynchronous and @ExponentialRetry annotations.



Note that the marks only indicate Eclipse sees the annotation and knows to apply aspects, but doesn't necessarily mean it does right things for these aspects. Another way verify compile-time weaving is to find compiled inner classes created by AspectJ. There should be several inner classes named AjcClosure#. If you see the AjcClosure# for WorkflowImp and ActivitiesClientImpl classes, it's a pretty positive sign that compile-time weaving is happening properly.

$ find target/classes -type f | grep Ajc
target/classes/.../FooActivitiesClientImpl$AjcClosure1.class
target/classes/.../FooWorkflowImpl$AjcClosure1.class
target/classes/.../FooWorkflowImpl$AjcClosure3.class
target/classes/.../FooWorkflowImpl$AjcClosure5.class
...

Load time weaving


Load time weaving means classes are compiled with standard Java compiler, while JVM is launched with "-javaagent" argument pointing to an aspectjweaver jar file at runtime. Load-time weaving require aop.xml to exist in classpath.

Eclipse, Maven consistency

With M2E eclipse plugin, the compile destination directory in Eclipse is read from Maven so both Eclipse and Maven compiles classes into the same directory. Therefore with M2E it becomes very important to keep Eclipse and Maven configured consistently. If not, after Maven compiles code Eclipse often doesn't know the classes are generated differently. As a result the same code in Eclipse can behavior differently.


Maven


Client code generation


Two things to be aware of to enable client code generation in Maven
  1. Add build tool dependency. Official Maven repository doesn't have it anymore so you need to install it from AWS Java SDK to local repository manually
  2. Configure APT maven plugin

<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-java-sdk-flow-build-tools</artifactId>
  <version>1.3.26</version>
</dependency>
...
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>apt-maven-plugin</artifactId>
  <version>1.0-alpha-5</version>
  <executions>
    <execution>
      <goals>
        <goal>process</goal>
      </goals>
    </execution>
  </executions>
</plugin> 

To verify, look for the 10 client classes in target/classes after a Maven build.
I had various troubles with Maven compiler plugin 3.0. Compiler plugin 2.5.1 or 2.5 works correctly.

Note that AspectJ maven plugin can do APT code generation as well so it seems simpler to have one AspectJ maven plugin do both APT code generation and compile-time weaving. However this does not work as the plugin can only do only one thing to a class. This means it can generate client code for workflow and activity interface, weave workflow implementation, but it can't continue to weave generated activity client code. As a result, the application mostly works except that @ExponentialRetry will be ignored. This problem has been discussed here.

Here is a fully working example of module that does APT code generation for workflow interface without AspectJ weaving.


Compile-time weaving


An example

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.4</version>
    <configuration>
        <complianceLevel>1.6</complianceLevel>
        <showWeaveInfo>true</showWeaveInfo>
        <verbose>true</verbose>
        <sources>
            <source>
                <basedir>${basedir}/src/main/java</basedir>
            </source>
            <source>
                <basedir>${basedir}/target/generated-sources/annotation</basedir>
        </sources>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>com.amazonaws</groupId>
                <artifactId>aws-java-sdk</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

To verify, look for the AjcClosure inner classes in target/classes after a Maven build. It's important to weave classes in both src/main/java and target/generated-sources/annotation directories because the APT generated activity client code also needs AspectJ weaving.

This module is an example of compile-time weaved workflow implementation as well as activity APT code generation.


Load-time weaving


Load-time weaving means there's nothing special for compilation but JVM needs to be launched with "-javaagent:" argument. In turn, Maven surefire plugin needs to be customized in order to run unit tests properly. In the POM file

    <properties>
<aspectj.version>1.6.11</aspectj.version>
</properties>
    ...
    <dependency>

        <groupId>org.aspectj</groupId>

           <artifactId>aspectjweaver</artifactId>

           <version>${aspectj.version}</version>

           <scope>runtime</scope>

    </dependency>
    ...
    <plugin>

        <groupId>org.apache.maven.plugins</groupId>

        <artifactId>maven-surefire-plugin</artifactId>

        <configuration>

            <argLine>-javaagent:${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar</argLine>

        </configuration>

    </plugin>




Comments

Popular posts from this blog

Spring, Angular and other reasons I like and hate Bazel at the same time

For several weeks I've been trying to put together an Angular application served Java Spring MVC web server in Bazel. I've seen the Java, Angular combination works well in Google, and given the popularity of Java, I want get it to work with open source. How hard can it be to run arguably the best JS framework on a server in probably the most popular server-side language with  the mono-repo of planet-scale ? The rest of this post walks through the headaches and nightmares I had to get things to work but if you are just here to look for a working example, github/jiaqi/angular-on-java is all you need. https://github.com/jiaqi/angular-on-java Java web application with Appengine rule Surprisingly there isn't an official way of building Java web application in Bazel, the closest thing is the Appengine rule  and Spring MVC seems to work well with it. 3 Java classes, a JSP and an appengine.xml was all I need. At this point, the server starts well but I got "No ...

Customize IdGenerator in JPA, gap between Hibernate and JPA annotations

JPA annotation is like a subset of Hibernate annotation, this means people will find something available in Hibernate missing in JPA. One of the important missing features in JPA is customized ID generator. JPA doesn't provide an approach for developer to plug in their own IdGenerator. For example, if you want the primary key of a table to be BigInteger coming from sequence, JPA will be out of solution. Assume you don't mind the mixture of Hibernate and JPA Annotation and your JPA provider is Hibernate, which is mostly the case, a solution before JPA starts introducing new Annotation is, to replace JPA @SequenceGenerator with Hibernate @GenericGenerator. Now, let the code talk. /** * Ordinary JPA sequence. * If the Long is changed into BigInteger, * there will be runtime error complaining about the type of primary key */ @Id @Column(name = "id", precision = 12) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "XyzIdGenerator") @SequenceGe...

Project Euler problem 220 - Heighway Dragon

This document goes through a Java solution for Project Euler problem 220 . If you want to achieve the pleasure of solving the unfamiliarity and you don't have a solution yet, PLEASE STOP READING UNTIL YOU FIND A SOLUTION. Problem 220 is to tell the coordinate after a given large number of steps in a Dragon Curve . The first thing came to my mind, is to DFS traverse a 50 level tree by 10^12 steps, during which it keeps track of a direction and a coordinate. Roughly estimate, this solution takes a 50 level recursion, which isn't horrible, and 10^12 switch/case calls. Written by a lazy and irresponsible Java engineer, this solution vaguely looks like: Traveler traveler = new Traveler(new Coordinate(0, 0), Direction.UP); void main() { try { traverse("Fa", 0); } catch (TerminationSignal signal) { print signal; } } void traverse(String plan, int level) { foreach(char c:plan) { switch(c) { case 'F': traveler.stepForward(); break; ca...