Saturday, 20 June 2009

Verifying proper HttpSession use for clusterable applications using eclipse, AJDT, AspectJ and maven

One question that I hear often at work is "how can we tell if a given application will work properly in a cluster?". The answer is always "test it", but honestly that's pretty hard to do really often. The tests usually rely on the app running in a cluster, session use, someone downing one of the servers as the functional tests run, etc. The really hard part, I think, is for an error to pop up that will be noticed. If session gets improperly synced across nodes, it might corrupt the data, but not in a way that's quickly noticed.



At work with our current clustering setup, objects put into session must implement java.io.Serializable, and the way you notify the server to sync your session is to call session.setAttribute(..). A hard to track down bug would be if you have a reference to an object, set it into session, and then use your reference to change the state of that object. What I set out to do is to create a fail-fast test where we can identify improper session use in the unit tests, before we even get close to deploying an app to a server.



Below is my first foray into the AOP java world using aspectj. I'm fully documenting this because, while there were many posts on aspectj, etc, I never found one that put it all together. I did originally start off trying to use the java annotations for aspectj, but after much struggle, I gave up and moved to using AJDT with great success.



The layout the solution consists of a corporate pom, a "auditor" library, and an example app that has unit tests. I won't actually show an example of the sample application because it simply is a bunch of tests, and has the corporate pom as it's parent. The code that runs the session tests has been put into a maven profile so that we can have one pom, but be able to mark applications as "clusterable" one by one. So, after each app updates to the given parent pom, to check if they are using session properly [1] they only have to run mvn clean test -Pclusterable.



One thing that I left as a TODO in the aspect is checking if the value being set into the session is actually serializable or not. I had actually written some code at work to do that, so I won't re-do it at home. ;-)



The algorithm


The high level idea of how the aspect works is that every time someone who implements HttpSession calls setAttribute(String,Object), we 1) can inspect the values being set to make sure that it's serializable 2) keep a reference to the session object for later 3) generate a hash of the attribute and store the hash in the session object [2]. After a test has successfully finished, we go over all the attributes in the session and check if the hash of each attribute matches the hash that we generated when setAttribute(..) was called. For the problems that we encounter, we throw a AssertionError - which is the same kind that junit throws.



Our corporate pom is below. Some important things to note are using the same version of aspectj everywhere, locking down the JSE version in the various plugins, and specifying the additional javaagent argument to surefire when using AOP.



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>ca.beernut.jim</groupId>
<artifactId>ParentPom</artifactId>
<version>1.0.1</version>
<packaging>pom</packaging>

<properties>
<aspectjVersion>1.6.4</aspectjVersion>
<jseVersion>1.5</jseVersion>
<surefireMemoryArgs>-Xmx512m -Xms128m</surefireMemoryArgs>
<auditorJarVersion>3.1.4</auditorJarVersion>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.1</version>
<configuration>
<source>${jseVersion}</source>
<complianceLevel>${jseVersion}</complianceLevel>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectjVersion}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${jseVersion}</source>
<target>${jseVersion}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.7</version>
<configuration>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
<ajdtVersion>none</ajdtVersion>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<forkMode>once</forkMode>
<argLine>${surefireMemoryArgs}</argLine>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<profiles>
<profile>
<id>clusterable</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:"${settings.localRepository}org/aspectj/aspectjweaver/${aspectjVersion}/aspectjweaver-${aspectjVersion}.jar"\
${surefireMemoryArgs}</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<configuration>
<aspectLibraries>
<aspectLibrary>
<groupId>ca.beernut.jim.aspects</groupId>
<artifactId>ProjectAuditor</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectjVersion}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectjVersion}</version>
</dependency>
<dependency>
<groupId>ca.beernut.jim.aspects</groupId>
<artifactId>ProjectAuditor</artifactId>
<version>${auditorJarVersion}</version>
</dependency>
</dependencies>
</profile>
</profiles>
</project>


Our ProjectAuditor pom's is as follows:



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ca.beernut.jim</groupId>
<artifactId>ParentPom</artifactId>
<version>1.0.0</version>
</parent>

<groupId>ca.beernut.jim.aspects</groupId>
<artifactId>ProjectAuditor</artifactId>
<version>3.1.4</version>
<packaging>jar</packaging>


<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectjVersion}</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.4</version>
</dependency>
<!-- This needs to be compile scope since we include it -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.4</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-eclipse-plugin</artifactId>
<configuration>
<!-- Override this because it is set to none in the parent -->
<ajdtVersion>1.5</ajdtVersion>
</configuration>
</plugin>
</plugins>
</build>

</project>


In ProjectAuditor, we had the 2 files: src/main/aspect/ca/beernut/jim/aspects/ClusterableHttpSessionAspect.aj



package ca.beernut.jim.aspects;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.apache.commons.lang.builder.HashCodeBuilder;

public final aspect ClusterableHttpSessionAspect {

/**
* Here is where we will keep track of all the session objects that were
* created. The key if the system's hashcode
*/
private ThreadLocal<Map<Integer, ClusterableHttpSession>> sessions = new ThreadLocal<Map<Integer, ClusterableHttpSession>>();

/** Default constructor that initalizes the thread local. */
public ClusterableHttpSessionAspect() {
sessions.set(new HashMap<Integer, ClusterableHttpSession>());
}

/**
* Create an interface type so that we can attach a member to it.
*/
declare parents : (HttpSession) implements ClusterableHttpSession;

private Map<String, Integer> ClusterableHttpSession.hashOnSet = new HashMap<String, Integer>();

public Map<String, Integer> ClusterableHttpSession.getHashOnSet() {
return hashOnSet;
}

public static interface ClusterableHttpSession {
}

/**
* Before anyone calls setAttribute, check that the pram is okay and keep
* track of it so later we can detect if it was changed outside of the set
* method.
*
* I could not get the method
* <code>after() returning (ClusterableHttpSession m): call(ClusterableHttpSession+.new(..))</code>
* to work for all mock sessions (see servletunit.HttpSessionSimulator), so
* I'm keeping track of the sessions in this method.
*/
before(ClusterableHttpSession m, String key, Object value) :
call(void HttpSession.setAttribute(..)) && target(m) && args(key,value) {

// TODO this is where you would put in your test to check if the "value"
// is serializable

// the value is serializable, so let's keep track of it's state
Map<Integer, ClusterableHttpSession> sessionList = sessions.get();

// let's keep track of this hash if we don't already have it
Integer sessionHash = Integer.valueOf(System.identityHashCode(m));
if (!sessionList.containsKey(sessionHash)) {
sessionList.put(sessionHash, m);
System.out.println("Added to thread local session: " + m);
}

Integer hash = hash(value);
m.getHashOnSet().put(key, hash);

System.out.println("For key: " + key + " and value " + value + " we have hash # of: " + hash);
}

pointcut afterUnitTest() : execution(void test*()) || @annotation(org.junit.Test);

/**
* After the unit test has finished, use the sessions that we have in thread
* local to check if the attribute has changed outside of the setAttribute
* call.
*
* This is just an "after returning" method because we only care about the
* state of the session if no exceptions were thrown.
*/
after() returning : afterUnitTest() {

Map<Integer, ClusterableHttpSession> sessionList = sessions.get();

System.out.println("After returning and sessions are : " + sessionList);

try {

// for each session, check the attributes that remain in the
// session against the ones that were set into it and make
// sure that the last matches

for (ClusterableHttpSession session : sessionList.values()) {
HttpSession httpSession = (HttpSession) session;
ClusterableHttpSession clusterableHttpSession = (ClusterableHttpSession) session;

for (Enumeration enumeration = httpSession.getAttributeNames(); enumeration.hasMoreElements();) {
String attributeName = (String) enumeration.nextElement();
Object value = httpSession.getAttribute(attributeName);

// we have to cast it here
Integer lastHash = clusterableHttpSession.getHashOnSet().get(attributeName);
Integer currentHash = hash(value);

if (!lastHash.equals(currentHash)) {
String message = "Object in session " + httpSession + " under key '" + attributeName
+ "' has hash of " + currentHash
+ ". However the prevous hash when setAttritbute was used was " + lastHash
+ ". Always call setAttribute on objects in session after changing their state.";
throw new AssertionError(message);
}
}
}
} finally {
clearThreadLocal();
}
}

/** If something was thrown, just clear the thread local for the next run. */
after() throwing : afterUnitTest() {
clearThreadLocal();
}

/**
* Clear out the thread local if the test returns properly or not
*/
private void clearThreadLocal() {

Map<Integer, ClusterableHttpSession> sessionList = sessions.get();

// clear out the thread local so we don't keep sessions from
// test to test
sessionList.clear();
}

/** A hash method to determine the state of the object. */
private Integer hash(Object obj) {
return Integer.valueOf(HashCodeBuilder.reflectionHashCode(obj));
}

}


Here is the second file with a special location and name: src/main/resources/META-INF/aop.xml



<aspectj>
<aspects>
<aspect name="ca.beernut.jim.aspects.ClusterableHttpSessionAspect" />
</aspects>
<weaver options="-verbose -showWeaveInfo -Xset:weaveJavaxPackages=true">
</weaver>
</aspectj>


Notes:

[1] With this code we're only checking code that gets run by their unit tests.

[2] See Inter-type Declarations



No comments:

Post a Comment