Thursday, February 07, 2008

Pod Racing: How to synchronize threads across multiple JVMs

Sometimes, you want to start the job in multiple JVMs only when all the VM has started. This scenario often comes up with multiplayer games. Let's take a pod racing game for example. The players need to start at the same time and each game server handles 1 player (for simplicity's sake)

Starting from Java 5, there's a CyclicBarrier to synchronize threads and is a perfect tool for this. However, it only handles threads in 1 VM. D'oh! Here's where Terracotta comes in. If you make that CyclicBarrier a shared object, Terracotta will handle the clustering for you, transparently (no API). All of a sudden, your CyclicBarrier object will be seen across all the VMs participated in the game.

Here's an example of how to do it. First, let's take a look at the PodRacer class
package demo;

import java.util.concurrent.CyclicBarrier;

public class PodRacer {
public final static int COUNT = 2;
private final CyclicBarrier barrier = new CyclicBarrier(COUNT);
private String name;

public PodRacer(String name) {
this.name = name;
}

public void ready() {
System.out.println(name + ": ready");
}

public void set() throws Exception {
/* in Terracotta world, all threads in different VMs will block here */
barrier.await();
}

public void go() throws Exception {
System.out.println(name + ": go");
Thread.sleep((int) (Math.random() * 5000) + 10);
System.out.println(name + ": arrived at " + System.currentTimeMillis());
}

public static void main(String[] args) throws Exception {
PodRacer racer = new PodRacer(System.getProperty("racer.name", "unknown"));
racer.ready();
racer.set();
racer.go();
}
}


Notice I hardcoded number of racers here to 2 but it can be made dynamic. If you run this class just like a normal Java program, it will block at racer.set() call because there's only 1 thread arriving at the barrier whereas it requires 2 for the barrier to be lifted. And if you don't use Terracotta underneath, it doesn't matter how many VMs you start, it will just block there.

Next, I'll add Terracotta into the mix and share the barrier object. That can be achieved by marking it in Terracotta configuration file tc-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<con:tc-config xmlns:con="http://www.terracotta.org/config">
<servers>
<server host="%i" name="localhost">
<dso-port>9510</dso-port>
<jmx-port>9520</jmx-port>
<data>terracotta/server-data</data>
<logs>terracotta/server-logs</logs>
</server>
</servers>
<clients>
<logs>terracotta/client-logs</logs>
</clients>
<application>
<dso>
<instrumented-classes/>
<roots>
<root>
<field-name>demo.PodRacer.barrier</field-name>
</root>
</roots>
</dso>
</application>
</con:tc-config>


And that's it. When I start 2 JVMs running PodRacer class with Terracotta, I will efficiently have 2 threads calling racer.set() on the shared barrier

To make it even easier to try out with Terracotta, there's a Maven 2 plugin that will handle starting up your project with Terracotta enabled. Here's the pom.xml of my project:


<?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>PodRacing</groupId>
<artifactId>PodRacing</artifactId>
<version>0.0.1</version>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.terracotta.maven.plugins</groupId>
<artifactId>tc-maven-plugin</artifactId>
<version>1.0.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>bootjar</goal>
</goals>
</execution>
</executions>

<configuration>
<processes>
<process nodeName="racer1"
className="demo.PodRacer"
jvmargs="-Dracer.name=Anakin"/>
<process nodeName="racer2"
className="demo.PodRacer"
jvmargs="-Dracer.name=Sebulba"/>
</processes>

</configuration>
</plugin>
</plugins>
</build>
<pluginRepositories>
<pluginRepository>
<releases />
<snapshots />
<id>terracotta</id>
<url>http://download.terracotta.org/maven2</url>
</pluginRepository>
</pluginRepositories>
</project>


See how I defined 2 racers through the process element in the plugin configuration. That will start 2 distinct VMs with Terracotta enabled. The output looks something like this:

[INFO] Starting DSO nodes
[INFO] Starting node racer1: c:\jdk\jdk1.6.0_02\jre/bin/java.exe -Dcom.tc.l1.modules.repositories=file:/C:/Users/hhuynh/.m2/repository/ -Dtc.nodeName=racer1 -Dtc.numberOfNodes=2 -Dtc.config=d:\work\workspace\projects\PodRacing\tc-config.xml -Dtc.classpath=file:/c:/Users/hhuynh/AppData/Local/Temp/tc-classpath37736.tmp -Dtc.session.classpath=/C:/Users/hhuynh/.m2/repository/org/terracotta/tc-session/2.5.0/tc-session-2.5.0.jar -Dcom.tc.l1.modules.repositories=file:/C:/Users/hhuynh/.m2/repository/ -Xbootclasspath/p:d:\work\workspace\projects\PodRacing\target\dso-boot.jar -Dracer.name=Anakin -cp d:\work\workspace\projects\PodRacing\target\classes; demo.PodRacer
[INFO] Starting node racer2: c:\jdk\jdk1.6.0_02\jre/bin/java.exe -Dcom.tc.l1.modules.repositories=file:/C:/Users/hhuynh/.m2/repository/ -Dtc.nodeName=racer2 -Dtc.numberOfNodes=2 -Dtc.config=d:\work\workspace\projects\PodRacing\tc-config.xml -Dtc.classpath=file:/c:/Users/hhuynh/AppData/Local/Temp/tc-classpath37737.tmp -Dtc.session.classpath=/C:/Users/hhuynh/.m2/repository/org/terracotta/tc-session/2.5.0/tc-session-2.5.0.jar -Dcom.tc.l1.modules.repositories=file:/C:/Users/hhuynh/.m2/repository/ -Xbootclasspath/p:d:\work\workspace\projects\PodRacing\target\dso-boot.jar -Dracer.name=Sebulba -cp d:\work\workspace\projects\PodRacing\target\classes; demo.PodRacer
[INFO] ------------------------------------------------------------------------

[INFO] [racer2] Sebulba: ready
[INFO] [racer1] Anakin: ready
[INFO] [racer1] Anakin: go
[INFO] [racer2] Sebulba: go
[INFO] [racer2] Sebulba: arrived at 1202406914274
[INFO] Finished node racer2
[INFO] [racer1] Anakin: arrived at 1202406915125
[INFO] Finished node racer1


Ouch, Anakin sucked!

Download the Maven project and try it out. Have fun.

P.S. More about sharing memory between JVMs here http://unserializableone.blogspot.com/2006/10/share-precious-heap-memory-accross.html or visit Terracotta