Monday, April 27, 2009

Sharing data of Spring beans with Terracotta

This is a quick demo showing how you would share data of Spring beans between different JVMs using Terracotta.

The example is based on the Voting Booth of Spring By Example website (a great website to learn Spring by the way)

In this example, I have 2 beans defined in application-context.xml


<bean id="recorder" class="org.terracotta.demo.LocalVoteRecorder" />

<bean id="votingBooth" class="org.terracotta.demo.VotingBooth">
<property name="voteRecorder" ref="recorder" />
</bean>


So simply, the 'votingBooth' bean has a reference to the 'recorder' bean which has a map of candidates and their votes.


public class VotingBooth {
VoteRecorder recorder = null;

public void setVoteRecorder(VoteRecorder recorder) {
this.recorder = recorder;
}

public void vote(Candidate candidate) {
recorder.record(candidate);
}

public void printResult() {
recorder.tallyVotes();
}
}

/* ************* */
public class LocalVoteRecorder implements VoteRecorder {
final static Log logger = LogFactory
.getLog(LocalVoteRecorder.class);
private final Map<Candidate, Integer> votesRecord = new ConcurrentHashMap<Candidate, Integer>();

public void record(Candidate candidate) {
int count = 0;

if (!votesRecord.containsKey(candidate)) {
votesRecord.put(candidate, count);
} else {
count = votesRecord.get(candidate);
}
count++;
votesRecord.put(candidate, count);
}

public void tallyVotes() {
for (Candidate candidate : votesRecord.keySet()) {
logger.info("Candidate '" + candidate + "' has "
+ votesRecord.get(candidate) + " votes.");
}
}
}


Spring will do its magic and inject the right data to into main program at runtime. To demo, I have the main function vote 5 times between 2 known candidates then print out the result in the end.


public class Main {

final static Log logger = LogFactory.getLog(Main.class);
final static Random rand = new Random();
final static Candidate[] candidates = new Candidate[] {
new Candidate("Hung"), new Candidate("Nick") };

public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
"/application-context.xml");

VotingBooth votingBooth = (VotingBooth) applicationContext
.getBean("votingBooth");

// vote 5 times
for (int i = 0; i < 5; i++) {
Candidate luckyCandidate = candidates[rand.nextInt(2)];
votingBooth.vote(luckyCandidate);
logger.info("voted for candidate " + luckyCandidate);
}

votingBooth.printResult();
}
}


So if you run this Spring app as is, it will print the result of that run and exit. Quite simple.

Now let say we want to maintain a voting record that would last beyond the life cycle of that run. And also, any subsequent runs should add to the vote tally. In other words, the data is shared between the nodes (JVMs) and persisted.

This is where Terracotta comes in. The data we're interested in sharing will be marked as "ROOT". In this case, it's the "votesRecord" field of the 'recorder' bean. This field will be transparently clustered by Terracotta, its value will be saved in Terracotta server and it will be faulted back into a new node as needed. There's no thirdparty function calls nor networking code, just plain Java. Here is what needed in Terracotta configuration file tc-config.xml:

<application>
<dso>
<instrumented-classes>
<include>
<class-expression>org.terracotta.demo.*</class-expression>
</include>
</instrumented-classes>
<roots>
<root>
<field-name>org.terracotta.demo.LocalVoteRecorder.votesRecord</field-name>
</root>
</roots>
</dso>
</application>


That piece of config tells Terracotta to instrument all classes under the org.terracotta.demo package and make "votesRecord" a root.

Since Terracotta has Maven plugin, I just use its plugin and run the example. The pom is here if you're interested. First, you need to start Terracotta server:

% mvn tc:start

Then you can run the example with Terracotta enable simply by:

% mvn tc:run

On first run, I got this result: Nick got voted twice


[INFO] [vote] INFO [main] voted for candidate Nick
[INFO] [vote] INFO [main] voted for candidate Hung
[INFO] [vote] INFO [main] voted for candidate Hung
[INFO] [vote] INFO [main] voted for candidate Nick
[INFO] [vote] INFO [main] voted for candidate Hung
[INFO] [vote] INFO [main] Candidate 'Hung' has 3 votes.
[INFO] [vote] INFO [main] Candidate 'Nick' has 2 votes.
[INFO] Finished node vote
[INFO] DSO processes finished
[INFO] Skipping stopping DSO Server
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------


On second run: Nick got voted one more time brings his total votes to 3


[INFO] [vote] INFO [main] voted for candidate Hung
[INFO] [vote] INFO [main] voted for candidate Hung
[INFO] [vote] INFO [main] voted for candidate Nick
[INFO] [vote] INFO [main] voted for candidate Hung
[INFO] [vote] INFO [main] voted for candidate Hung
[INFO] [vote] INFO [main] Candidate 'Hung' has 7 votes.
[INFO] [vote] INFO [main] Candidate 'Nick' has 3 votes.


This shows that the votes are indeed shared between 2 separate runs. To see the shared data live, you can use Terracotta Developer Console to look at the roots:

Run this Maven command to start the console:

% mvn tc:dev-console



You can check out the whole project from Subversion: http://svn.terracotta.org/svn/forge/projects/TerracottaSpringDemo
Post a Comment