Yesterday I was spending some time thinking about the possibilities to cluster applications written in "JRuby":http://jruby.codehaus.org/ with "Terracotta":http://www.terracotta.org/.
Sounds like a crazy idea? Well, I don't know. Perhaps it is. But thinking ahead a bit, with perhaps future deployments of "Ruby on Rails":http://www.rubyonrails.org/ applications etc. on JRuby makes it a bit more interesting.
Anyway, let's give it a try.
h1. Chatter sample application
First let's start with writing a **very** simple chat application in JRuby.
require'java'classChatter# set the name and create a List to hold our messages
definitialize@name=ARGV[0]@messages=java.util.ArrayList.newputs"--- Hi #{@name}. Welcome to Chatter. Press Enter to refresh ---"end# takes text that the user enters
# 1) if enter is pressed -> show the latest messages (refresh)
# 2) if text is entered -> prepend the user name and add it to
# the list of messages
# 3) display the messages
defrunwhiletruedoprint"Enter Text>>"text=STDIN.gets.chompiftext.length>0then@messages.add"[#{Time.now} -- #{@name}] #{text}"puts@messageselseputs@messagesendendendendChatter.new.run
Of course, this application is completely useless. It is in process, single threaded and only recieves input from STDIN, which means that it can only be used by one single user at a time.
But, what if we could make a single instance of the @messages list available on the network and then run multiple instances of the application (each on its own JVM instance, even on multiple machines) and have each one of them use this shared list?
This would solve our problem and this in actually exactly what Terracotta could [conceptually] do for us. But Terracotta is a Java infrastructure service without any bindings or support for JRuby. So how can we proceed?
h1. Terracotta for JRuby
I can see two different ways we can bridge JRuby and Terracotta:
h2. Create a pattern language
We could hook into and extend the Terracotta pattern matching language (the language that is used to pick out the shared state and the methods modifying it) to support the Ruby syntax. This would mean allowing the user to define the patterns based on the Ruby language but then, under the hood, actually map the Ruby syntax to the real Java classes that are generated by JRuby (this assumes using the compiler and not the interpreter). The benefit here is that it would be "transparent", in the same way as it is for regular Java applications. This is perhaps the best solution long term, but requires quite a lot of work and requires a fully working JRuby compiler. The development of the JRuby compiler has just started. When I tried it today it did not even compile the samples shipped with the distribution, so the Terracotta support for the compiler naturally has to wait until the implementation gets more complete and stable.
h2. Create a JRuby API
The minimal set of abstractions we need in this API is:
1. State sharing: Be able to pick out the state that should be shared - e.g. the top level "root" object of a shared object graph
2. State guarding: Be able to safely update this graph within the scope of a transaction
Ok, let's try to design this API.
h1. Designing the JRuby API
Most people are probably not aware of that Terracotta actually has an API that can do all this (and much more) for us. It is well hidden in the "SVN repository":http://svn.terracotta.org/fisheye/browse/Terracotta and is used internally as the API for the hooks, added to the target application during the bytecode instrumentation process, to call.
The class in question is the: "ManagerUtil":http://svn.terracotta.org/fisheye/browse/Terracotta/branches/2.2.0/community/open/code/base/dso-l1/src/com/tc/object/bytecode/ManagerUtil.java.
So, everything we need is in the ManagerUtil class. But before we roll up our sleeves and start hacking on the glue code, let's take a step back and think through what we want out of the API from a user's perspective.
First we need to be able to create a "shared root", e.g. create a top-level instance of a shared object graph. One way of doing it would be to create some sort of factory method that can do the heavy lifting for us, similar to this:
@messages=createRootjava.util.ArrayList.new
Here we told some factory method createRoot to create a root instance of the type java.util.ArrayList. Seems reasonable I think.
The other thing we need to do is to create some sort of transactions. We want to be able to start a transaction, lock the [shared] instance being updated, update it, unlock the instance and finally commit the transaction. Here is some basic pseudo code:
All steps except 'modify target' is done by infrastructure code. Code that has nothing to do with your business logic, code that we want to eliminate and "untangle" as much as possible.
In Ruby, a common design pattern to accomplish this is to use a method that takes a "block/closure":http://en.wikipedia.org/wiki/Closure_(computer_science). Using this pattern we could design the API to look something like this:
guard@messagesdo@messages.addmsgend
Here the semantics are:
* the @messages list is guarded for concurrent access (both in a single JVM or in the cluster)
* the 'do' keyword takes the lock (on @messages) and initiates the transaction (unit of work)
* all updates to @messages that are done within the 'do-end' block are recorded in a change set (and are guaranteed to be done in isolation)
* when the 'end' of the block is reached then the transaction is committed, the change set is replicated and the lock (on @messages) is released
h1. Implementing the JRuby API
Now, let's take a look at the ManagerUtil class. It has a lot of useful stuff but the methods that we are currently interested in has the following signatures:
Based on these methods we can create the following JRuby module implementing the requirements we outlined above (and let's put it in a flle called 'terracotta.rb' to be able to easily include it into the applications we want) :
# Usage:
#
# # create an instance of 'foo' as a DSO root this
# # means that it will be shared across the cluster
# foo = DSO.createRoot "foo", Foo.new
#
# # update 'foo' in a guarded block, get result back
# result = DSO.guard foo, DSO::WRITE_LOCK do
# foo.add bar
# foo.getSum
# end
TC=com.tc.object.bytecode.ManagerUtilmoduleDSO# The different lock types
WRITE_LOCK=com.tc.object.bytecode.Manager::LOCK_TYPE_WRITEREAD_LOCK=com.tc.object.bytecode.Manager::LOCK_TYPE_READCONCURRENT_LOCK=com.tc.object.bytecode.Manager::LOCK_TYPE_CONCURRENT# Creates a Terracotta shared root, 'name' is the name of the root
# (can be anything that uniquily defines the root), 'object' is an
# instance of the object to be shared. If the root the given name
# already exists it simply returns it.
defDSO.createRoot(name,object)guardWithNamedLockname,WRITE_LOCKdoTC.lookupOrCreateRootname,objectendend# Creates a transaction and guards the object (passed in as the
# 'object' argument) during the execution of the block passed into
# the method. Similar to Java's synchronized(object) {...} blocks.
# Garantuees that the critical section is maintained correctly across
# the cluster. The type of the lock can be one of: DSO:WRITE_LOCK,
# DSO::READ_LOCK or DSO::CONCURRENT_LOCK (default is DSO:WRITE_LOCK).
defDSO.guard(object,type=WRITE_LOCK)TC.monitorEnterobject,typebeginyieldensureTC.monitorExitobjectendend# Creates a transaction and guards the critical section using a virtual
# so called 'named lock. It is held during the execution of the block
# passed into the method. Garantuees that the critical section is
# maintained correctly across the cluster. The type of the lock can
# be one of: DSO:WRITE_LOCK, DSO::READ_LOCK or DSO::CONCURRENT_LOCK
# (default is DSO:WRITE_LOCK)8.
defDSO.guardWithNamedLock(name,type=WRITE_LOCK)TC.beginLockname,typebeginyieldensureTC.commitLocknameendend# Dispatches a Distributed Method Call (DMI). Ensures that the
# particular method will be invoked on all nodes in the cluster.
defDSO.dmi(object,methodName,arguments)TC.distributedMethodCallobject,methodName,argumentsendend
The ManagerUtil has a whole bunch of other useful and cool methods, such as for example optimisticBegin, optimiticCommit and deepClone for creating optimistic concurrency transactions, etc. But I'll leave these for a later blog post.
h1. Creating a distributed version of the Chatter application
Great. Now we have a JRuby API with all the abstractions needed to create a distributed version of our little chat application. What we have to do is simply to create the @messsages variable using the factory method for creating roots in the API. Then we also have to make sure that we guard the updating of the shared java.util.ArrayList using a guarded block.
Let's take a look at the final version of the application.
require'java'load'terracotta.rb'classChatterdefinitialize@name=ARGV[0]@messages=DSO.createRoot"chatter",java.util.ArrayList.newputs"--- Hi #{@name}. Welcome to Chatter. Press Enter to refresh ---"enddefrunwhiletruedoprint"Enter Text>>"text=STDIN.gets.chompiftext.length>0thenDSO.guard@messagesdo@messages.add"[#{Time.now} -- #{@name}] #{text}"puts@messagesendelseputs@messagesendendendendChatter.new.run
Pretty simple and fairly intuitive, right?
**Note 1**: Actually, we could have made it even more simple if we had taken a java.util.concurrent.LinkedBlockingQueue instead of a regular java.util.ArrayList as the list to hold our messages. If we would have done that then we could have skipped the DSO.guard block altogether since the Java concurrency abstractions are natively supported by Terracotta. But then I would have missed the opportunity to show you how to handle non-thread-safe data access.
**Note 2**: It currently only works for sharing native Java classes. In other words, you can currently **not** cluster native JRuby constructs since it would mean cluster the internals of the JRuby interpreter (which is a lot of work and most likely not possible without rewriting parts of the interpreter). However, one feasible approach would be to not use the JRuby interpreter but the JRuby compiler and cluster its generated Java classes - but unfortunately the compiler is not ready for general use yet (see the footnote at the bottom).
h1. Enable Terracotta for JRuby
In order to enable Terracotta for JRuby we have to add a couple of JVM options to the startup of our application, or add them directly in the jruby.(sh|bat) script.
You have to change:
"Hey! What's up with that tc-config.xml file? Do I have to write that? I hate XML!"
Yes, unfortunately you have to feed Terracotta with a tiny bit of XML. This can perhaps be eliminated in the future (and replaced with a couple of command line options more or a JRuby config). But for now you have to write an XML file that looks like this:
As you can see it contains the name of the server where the Terracotta server resides, the path to the logs and a statement that says; include all classes for instrumentation. That's it.
Now let's run it.
h1. Run the distributed Chatter
Here is an sample of me trying it out with my wife Sara. It shows my session window:
-->jruby./chat.rbJonas---HiJonas.WelcometoChatter.PressEntertorefresh---EnterText>>Hithere.Isitworking?[[FriFeb0213:07:19CET2007--Jonas]Hithere.Isitworking?]EnterText>>[[FriFeb0213:07:19CET2007--Jonas]Hithere.Isitworking?,[FriFeb0213:08:09CET2007--Sara]Ithinkso,IcouldseeyourmessagewhenIclickedEnter]EnterText>>Awesome!!Isn't this amazingly cool? Terracotta rocks!
[[Fri Feb 02 13:07:19 CET 2007 -- Jonas] Hi there. Is it working?
, [Fri Feb 02 13:08:09 CET 2007 -- Sara] I think so, I could see your message when I clicked Enter
, [Fri Feb 02 13:08:59 CET 2007 -- Jonas] Awesome!! Isn'tthisamazinglycool?Terracottarocks!]EnterText>>[[FriFeb0213:07:19CET2007--Jonas]Hithere.Isitworking?,[FriFeb0213:08:09CET2007--Sara]Ithinkso,IcouldseeyourmessagewhenIclickedEnter,[FriFeb0213:08:59CET2007--Jonas]Awesome!!Isn't this amazingly cool? Terracotta rocks!
, [Fri Feb 02 13:10:17 CET 2007 -- Sara] What do you mean? This is it?
]
Enter Text>>Well, yeah...
[[Fri Feb 02 13:07:19 CET 2007 -- Jonas] Hi there. Is it working?
, [Fri Feb 02 13:08:09 CET 2007 -- Sara] I think so, I could see your message when I clicked Enter
, [Fri Feb 02 13:08:59 CET 2007 -- Jonas] Awesome!! Isn'tthisamazinglycool?Terracottarocks!,[FriFeb0213:10:17CET2007--Sara]Whatdoyoumean?Thisisit?,[FriFeb0213:10:36CET2007--Jonas]Well,yeah...]EnterText>>
Well, I hope that you find it a bit more exciting...
Feel like helping out? Drop me a line.
----
h1. And one more thing...
I also had some fun porting the JTable sample in the Terracotta distribution. It worked out nicely. I only had one problem and that was that I encountered a JRuby bug when trying to create a multi-dimensional java.lang.Object array. I was able to work around the bug by creating the array using reflection, but this unfortunately had the effect of making the final code much longer. Anyway, here is the code in case you want to try it out:
require'java'load'terracotta.rb'# need to create the object arrays using reflection
# due to a bug in JRuby
tableHeader=java.lang.Object[].new(4)tableHeader[0]="Time"tableHeader[1]="Room A"tableHeader[2]="Room B"tableHeader[3]="Room C"dim=java.lang.reflect.Array.newInstance(java.lang.Integer::TYPE,2)java.lang.reflect.Array.setInt(dim,0,9)java.lang.reflect.Array.setInt(dim,1,3)tableData=java.lang.reflect.Array.newInstance(java.lang.Object,dim)tableData[0][0]="9:00"tableData[1][0]="10:00"tableData[2][0]="11:00"tableData[3][0]="12:00"tableData[4][0]="1:00"tableData[5][0]="2:00"tableData[6][0]="3:00"tableData[7][0]="4:00"tableData[8][0]="5:00"# create the model as a DSO root
model=DSO.createRoot"jtable.model",javax.swing.table.DefaultTableModel.new(tableData,tableHeader)table=javax.swing.JTable.new(model)frame=javax.swing.JFrame.new"Table Demo"frame.getContentPane().add(javax.swing.JScrollPane.new(table))frame.setDefaultCloseOperationjavax.swing.JFrame::EXIT_ON_CLOSEframe.setSize500,200frame.packframe.setVisibletrue