First steps to Akka Remote Actors
I've been working on my personal project, Lifthub, which uses Lift, Akka, Gitorious etc. I'm pretty new to Akka and even the concept of Actors, so it took me quite a while to understand it and get the code running.
Here are first steps to Akka Remote Actors for those who aren't familiar with Akka and/or Actors. Actually, Akka has a lot of great documentation (and sometimes overwhelming for beginners), so I'll just put pointers to it when appropriate.
Set up a Remote Actor
An Akka Remote Actor listens on a port and communicates with clients via TCP, so it's pretty much a server. I'll call it "Server" depending on the context in this artricle.
According to a page on the official site, there are two ways to start up a Server, but one of the two is deprecated as of Akka 1.1, so we should use the other one, server side setup.
Here is a sample code included in the git repository of Akka. Client code is also included.
Define messages
The example page on the Akka page sends a String and receives a String as a response, which isn't that practical. Another page about "Actors" (not specific to Remote Actors) has a section about messages.
In short, messages must be immutable, so primitives or case classes are recommended. I created cases classes extending a sealed trait.
sealed trait SampleEvent // Messages case class FooEvent(f1: Int, f2: String) extends SampleEvent case class BarEvent(f1: String) extends SampleEvent
Basic structure
Assume the following scenario.
- Server receives a message from an Actor client
- Server passes it to a helper object (call it Helper)
- Helper does something and returns a Box value
- Server returns it as a response.
The code would be like the following:
// Server class SomeActor extends Actor { def receive = { case FooEvent(f1, f2) => val res: Box[Int] = Helper.doFoo(f1, f2) self.reply(res) case BarEvent(f1) => // .... } } // Client object Client { val server = remote.actorFor(....) // Returns the same type as Helper.doFoo def foo(f1: Int, f2: String): Box[Int] = { server !! (FooEvent(f1, f2) match { case Some(x) => x match { case box: Box[Int] => box // causes a warning because of type erasure case _ => Failure("unknown response") } case None => Failure("timeout") } } }
Define responses
The above code works, but I'm not so sure that it's good. I think it's better to define case classes instead of returning Box directly.
trait SampleResponse // Messages case class FooResponse(res: Box[Int]) extends SampleResponse case class BarResponse(res: Box[String]) extends SampleResponse
Now, the client code looks like the following.
// Client object Client { val server = remote.actorFor(....) // Returns the same type as Helper.doFoo def foo(f1: Int, f2: String): Box[Int] = { server !! (FooEvent(f1, f2) match { case Some(x) => x match { case FooResponse(box) => box // type safe case _ => Failure("unknown response") } case None => Failure("timeout") } } }
This example just eliminates the warning, but you want to include more information in a response in other scenarios, and case classes would help for the purpose.
Other things to consider
Serialization
Messages and responses are passed via network, and must be able to serializable. Here is a page dedicated to serialization.
At first, I was trying to pass a Lift mapper class and got the error shown below:
java.io.NotSerializableException: net.liftweb.util.FatLazy at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1180) at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1528) at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1493) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1416) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1174) at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1528) at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1493) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1416) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1174) at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1528) at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1493) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1416) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1174) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:346) at akka.serialization.Serializer$Java$class.toBinary(Serializer.scala:58) at akka.serialization.Serializer$Java$.toBinary(Serializer.scala:53) at akka.remote.MessageSerializer$.serialize(MessageSerializer.scala:72) at akka.serialization.RemoteActorSerialization$.createRemoteMessageProtocolBuilder(SerializationProtocol.scala:329) at akka.remote.netty.RemoteClient.send(NettyRemoteSupport.scala:176) at akka.remote.netty.NettyRemoteClientModule$$anonfun$send$1.apply(NettyRemoteSupport.scala:59) at akka.remote.netty.NettyRemoteClientModule$$anonfun$send$1.apply(NettyRemoteSupport.scala:59) at akka.remote.netty.NettyRemoteClientModule$class.withClientFor(NettyRemoteSupport.scala:86) at akka.remote.netty.NettyRemoteSupport.withClientFor(NettyRemoteSupport.scala:504) at akka.remote.netty.NettyRemoteClientModule$class.send(NettyRemoteSupport.scala:59) at akka.remote.netty.NettyRemoteSupport.send(NettyRemoteSupport.scala:504) at akka.remote.netty.NettyRemoteClientModule$class.send(NettyRemoteSupport.scala:59) at akka.remote.netty.NettyRemoteSupport.send(NettyRemoteSupport.scala:504) at akka.actor.RemoteActorRef.postMessageToMailboxAndCreateFutureResultWithTimeout(ActorRef.scala:1147) at akka.actor.ScalaActorRef$class.$bang$bang(ActorRef.scala:1316) at akka.actor.RemoteActorRef.$bang$bang(ActorRef.scala:1117)
Configuration
You can fine tune Akka Remote Actors by using a config file. I haven't looked at it yet, but I'll use it and write an entry about it.