Wednesday, July 22, 2009

Darkstar Clojure together at last

Well, I'm on holiday so I'm not writing code... Much code.

Current situation is this:

I'm still auditioning Darkstar, which is now at the point that I need a convincing approximation of the current server API in it. Fairly close now. To be fair, it was pretty much 90% there just with core operations... If you're just writing a message router it's pretty much all in. Still some questions to be answered through experimentation; things like:

  1. How reliable and timely is the disconnect awareness if a user's channel goes down? Is it only picked up the next time the server tries to exercise that channel? Is there an implicit heartbeat? Is there a disconnection event from under the covers? How reliable is it?
  2. How easy is it to access the underlying data structures? I frequently need to audit our data records and SQL or similar would be nice. Otherwise it's going to be very hard to pass this off when we're done prototyping.
  3. If a user reconnects under the same identity, will the system reattempt previously failed transactions to that user? Sort of like a poison message queue, I guess, that retries when there's optimism that the situation might have changed is what I'm after.
So anyway, answering these questions isn't so much a matter of coding. What's occupying me at the moment is a different set of constraints:

I'm running the Darkstar server on Ubuntu, and running our eventual client from WPF at this point. Mono doesn't particularly run WPF, certainly not 3.5 SP1 which is where a few of our core features sit, so the client needs to run on Vista. I could carry two laptops around all the time (and last time Stu and I hacked on the client we just used his laptop for the client and mine for the server; was fine), but that's not sustainable or convenient. In particular, it makes it hard to connect them when there's no network whereas that's not an obstacle for client/server on the same machine.

Darkstar's pretty agnostic - there is some undetermined specificity about connecting to the server but once you're in it's just bytes on the channel. No strong contracts, no typing.

Not having investigated that aforementioned specificity I can't touch it with Erlang yet, but there's a whole bunch of Java clientry in the basic distro. Only problem is Java gives me hives, and I'm already writing it on the server. The current piece of work I'm undertaking is:
  1. Very thread distributed.
  2. Highly message oriented.
  3. Highly flexible.
I wrote a C# client for load testing which embodies hundreds to thousands of client instances connecting to the same server. Was quite inflexible, although still better than Java would have been because at least I could hotswap behaviours through first class functions easily. I figure with all the available JVM langs out there it should be possible to come up with a solution to this loose spec:

Many clients connect to the server. Their behaviour is highly configurable at runtime. When they undertake actions which should affect other members, that expectation is shared with those members, who coordinate with the sending client in correctly timing and monitoring the result and status of the actions.

I figure that's an interesting sort of interaction topology - there's direct message passing happening in the client cloud, and a single duplex channel to the server for each client. I'd love to do the first type of interaction in Erlang, and the second might as well be pretty much anything (although, to be fair, Erlang is also pretty shit hot at byte marshalling).

So what have we got in the pot now? Erlang style message parsing, direct Java interop and hopefully fairly easy to modify and extend. Clojure? Why not... I'm playing with Lisp at the moment and it certainly seems like the pick of the field for extensibility. How's its interop?

A bit like this:

(import '(com.sun.sgs.client.simple SimpleClient SimpleClientListener))
(import '(com.sun.sgs.client ClientChannelListener))
(def listener (proxy [SimpleClientListener ClientChannelListener][]
(loggedIn[] (print "Logged in yay"))
...All the other methods you feel like implementing from either of the two proxied interfaces
))
(def client (new SimpleClient listener))
(. client login (new Properties))

That doesn't work, of course, because you need to set up the properties with the actually relevant properties. Might need some kind of wrapper to let me pass key value pairs to a faux constructor, or that might already be in the Java interfaces - I haven't checked yet.

Anyway, should we have a look at the above code? Seems fairly self explanatory, doesn't it? Yay! I'm a smug lisp weenie! At last!

The first two lines should be clear enough - basic Java import statements, with the exception that having specified a package you can list as many classes from it as you like. This is a subtle sort of benefit of postfix syntax, that you can often change an infix idiom to be infinitely extensible without rephrasing.

(def listener (proxy [SimpleClientListener ClientChannelListener][]
(loggedIn[] (print "Logged in yay")))) is loosely equivalent to:

SimpleClientListener listener = new SimpleClientListener(){
public void loggedIn(){
System.out.println("Logged in yay");
}
};

But with one major difference: The Clojure proxy is implementing two interfaces without any extra work needing to be done. To do this in Java I'd need to create a new file for a specific interface, extending both SimpleClientListener and ClientChannelListener. Let's call it SimpleClientChannelListener, which is more dignity than it deserves. A few more of those and nobody will ever be able to navigate through my little project.

(def client (new SimpleClient listener))

I genuinely do think that this is sufficiently similar that it doesn't need any explanation. I'll just point out that although the lisp version has more parenthesies it is shorter and has much less syntax than:

SimpleClient client = new SimpleClient(listener);

The last one, which is interesting in the way it directly maps java idiom and syntax into lisp, is;
(. client login (new Properties))
which means
client.login(new Properties());

Interesting, isn't it? Once you've made the small acclimitization away from infix, it even reads the same (Java still has one more character).

I'm going to stop the Java/Clojure comparison at this point because this was really the only section of the project for which it was remotely fair. Once the clients start messaging each other to notify send and request receipt, Java will turn into a terrible mess of cross-threading and I don't want to write it even for demonstration purposes. Nor would it be simple to extend or modify.