In
one of my previous posts I mentioned
Terracotta, an open source clustering solution for Java. Its main advantage over traditional Java solutions in use (like
RMI) is that it works as middleware between a JVM and an application, enabling transparent sharing of Java objects across the network. The only requirement for a programmer is to execute operations on shared objects in
synchornized context, which allows you to build distributed applications or refactor already existing ones quickly.
Inspired by
Jonas Bonér's experiments with JRuby I decided to give it a try with
JScheme, an open source
Scheme implementation running on JVM. I chose JScheme over other Scheme implementations (like
Kawa or
SISC) because of its very simple, clear and elegant
interface to Java objects. In fact, since Terracotta operates on the JVM level, you can use it to cluster any application written in any language, as long as it compiles to Java bytecode and provides an interface to communicate with Java objects and classes.
First, you need to
download and install Terracotta (current stable version is 2.7.0). Then, you have to start a server with
start-tc-server.sh script. In Linux, if you encounter any strange errors running any of the scripts in the
bin directory of your Terracotta installation, change the header of a problematic script from
#!/bin/sh to
#!/bin/bash - this should help to solve the problem. The server manages all the shared objects in Terracotta cluster and needs to be started before any client applications are run.
Next, you need to prepare a client configuration in the form of an XML file. I used a configuration provided by Jonas and stored it in
tc-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<tc:tc-config xmlns:tc="http://www.terracotta.org/config">
<servers>
<server name="localhost"/>
</servers>
<clients>
<logs>%(user.home)/terracotta/jtable/client-logs</logs>
<statistics>%(user.home)/terracotta/jtable/client-logs</statistics>
</clients>
<application>
<dso>
<instrumented-classes>
<include>
<class-expression>*..*</class-expression>
</include>
</instrumented-classes>
</dso>
</application>
</tc:tc-config>
Now you can start JScheme interpreter hosted by Terracotta:
java -Xbootclasspath/p:[terracotta boot jar] -Dtc.config=tc-config.xml -Dtc.install-root=[terracotta install dir] -jar jscheme.jar
Boot jar is a stub jar file that starts a Terracotta client and connects to the server before the main application is started. You should be able to find it in
lib/dso-boot folder of your Terracotta installation directory. If it isn't there, you can generate it with
make-boot-jar.sh script found in Terracotta
bin folder.
Now when the whole working environment has been set up, you can use
com.tc.object.bytecode.ManagerUtil class to create a shared root object. Unfortunately Jonas's method which uses LOCK_TYPE_WRITE static field of
com.tc.object.bytecode.Manager class to perform a write lock during this operation fails to work. It causes some strange error about missing
com.tc.object.event.DmiManager class, which seems to be a problem
even with Jonas's JRuby example itself. A quick solution to this problem is to define locks in a way they are defined in
com.tc.object.lockmanager.api.LockLevel class:
(define TC_READ_LOCK 1)
(define TC_WRITE_LOCK 2)
Now let's define a sample object to share. It can be, for example, an ArrayList:
(define l (ArrayList.))
Next you need to create a shared root object named "list" that will hold the
l object:
(import "com.tc.object.bytecode.ManagerUtil")
(ManagerUtil.beginLock "list" TC_WRITE_LOCK)
(define ob (ManagerUtil.lookupOrCreateRoot "list" l))
(ManagerUtil.commitLock "list")
Terracotta provides a great debugging tool called Terracotta Administrator Console to analyze the objects held by the server. Start the console by running
admin.sh script found in
bin folder of the Terracotta installation directory, then connect to
localhost and go to
Cluster object browser. You should see an empty ArrayList on the object list.
Now let's add a new value to the shared object:
(ManagerUtil.monitorEnter ob TC_WRITE_LOCK)
(.add ob 1)
(ManagerUtil.monitorExit ob)
Go back to Terracotta Administrator Console, select the shared ArrayList and press F5 to refresh the view. You should see that now it holds a single value: 1.
Now start another scheme shell instance and try to read the first list value on the shared list:
(define TC_READ_LOCK 1)
(define TC_WRITE_LOCK 2)
(import "com.tc.object.bytecode.ManagerUtil")
(ManagerUtil.beginLock "list" TC_WRITE_LOCK)
(define ob (ManagerUtil.lookupOrCreateRoot "list" (ArrayList.)))
(ManagerUtil.commitLock "list")
(.get ob 0)
The return value is 1. Sweet!
I wrote a small library for JScheme that allows you to perform the basic operations of creating shared root objects in Terracotta and modifying them with read and write locks. You can download it from
this location.
To do the list operations described above you can simply do:
(load "jstc.scm")
(define ob (create-root "list" (ArrayList.)))
(sync-write (lambda () (.add ob 1)) ob)
in one shell and then read the list value in another shell:
(load "jstc.scm")
(define ob (create-root "list" (ArrayList.)))
(.get ob 0)
Have fun!