« Back
in akka Scala read.

Running a Remote Creator Actor with Akka and Scala.

Today I wanted to take a look at having remote actors on a different JVM and how we can push work onto another machine or another system altogether. I think this is advantageous for many reasons including:

  1. Being able to scale out
  2. Extending functionality of current systems
  3. Dealing with concurrency in a way that's more readable and less error prone
  4. Cost - It's open source

In a previous post I mentioned how to get Scala running in ColdFusion. This is one of the reasons I started looking at Scala as well; To provide extra functionality to CF without breaking or changing the existing code base too much.

I borrowed a lot of the code from TypeSafe's Activator examples. As mentioned last time, it comes with plenty of tutorials and bundles the build tool as well.

Actors On Different JVMs

The example here will look at starting a "Creator" system on one JVM which spawns actors to do some kind of work. The work will be an emailing task.

Another "Lookup" system will connect to the remote Creator and feed it some work. The lookup system will be run in a terminal in this example but it can also be called from other systems such as CF. What we would like to achieve is to fire off the work to the remote Creator system and then close off to free memory. Since the Creator system is the work horse, we can boost up its heap size and memory footprint to have more resources available.

Dependencies

Your build.sbt will require another package to enable remoting:

libraryDependencies ++= Seq(  
  "com.typesafe.akka" %% "akka-remote" % "2.3.8"
)

libraryDependencies += "com.typesafe" % "config" % "1.2.1"  

Emailer Actor

First we define what kind of work is possible in EmailOperations.scala:

trait EmailOperation  
case class SendMail(subject:String,body:String,from:String,to:String) extends EmailOperation  

Pretty self explanatory. The Actor that actually does the work is in Emailer.scala

import akka.actor.Props  
import akka.actor.Actor

class Emailer extends Actor {  
  def receive = {
    case SendMail(subject,body,from,to) =>
      Thread.sleep(5000)
      println(s"Email Sent with Parameters: $subject $body $from $to")
      context.stop(self)
    case _ =>
      println("Invalid Operation")
      context.stop(self)
  }
}

In this example we're not really sending the E-mail but we're going to simulate it taking 5 seconds to do so. This is so we know that the computation is still happening even when the caller has shut itself down.

Once the Emailer has done its work, it'll stop itself which is demonstrated by context.stop.

Creator Actor

The creator actor will using the following config which sits in /src/main/resources/creator.conf

akka {  
  # LISTEN on tcp port 2552
  remote.netty.tcp.port = 2552
  actor {
    provider = "akka.remote.RemoteActorRefProvider"
  }

  remote {
    netty.tcp {
      hostname = "127.0.0.1"
    }
  }
}

This essentially says that the Creator will sit on localhost on port 2552. This is where it will wait and listen for work.

The EmailerCreator.scala class spawns new instances of Emailer actors when it receives an EmailOperation as follows:

import akka.actor.Actor  
import akka.actor.Props

class EmailerCreator extends Actor {  
  def receive = {
    case op: EmailOperation =>
      val emailer = context.actorOf(Props[Emailer])
      emailer ! op
    case _ =>
      println("Undefined Message in EmailerCreator")
  }
}

To get the whole Creator system running and waiting for work we start it up with this application starter:

import com.typesafe.config.ConfigFactory  
import akka.actor.ActorSystem  
import akka.actor.Props

object EmailerCreatorApplication{  
  def main(args: Array[String]): Unit = {
      startRemoteCreationSystem()
  }

  def startRemoteCreationSystem(): Unit = {
    val system =
      ActorSystem("EmailerCreationSystem", ConfigFactory.load("creator"))
    val actor = system.actorOf(Props[EmailerCreator], name = "emailerCreator")

    println("Started Email Creation System")    
  }
}

Running this in one terminal yields a result like this:
Started Creator Akka Actor

Lookup Actor

Now we have to look up the Creator system we just ran and give it some work to do.
First we need to give it a configuration remotelookup.conf:

akka {  
  remote.netty.tcp.port = 0
  actor {
    provider = "akka.remote.RemoteActorRefProvider"
  }

  remote {
    netty.tcp {
      hostname = "127.0.0.1"
    }
  }
}

It looks almost identical to the last one. Except we can change the IP address here of where it's supposed to run. Leaving the remote.netty.tcp.port = 0 means that the actor will just choose a random port.

The lookup actor LookupEmailerCreator.scala looks like this:

import akka.actor.Actor  
import akka.actor.ActorIdentity  
import akka.actor.ActorRef  
import akka.actor.Identify

class LookupEmailerCreator(path: String) extends Actor {

  sendIdentifyRequest()

  def sendIdentifyRequest(): Unit = {
    context.actorSelection(path) ! Identify(path)
  }

  def receive = identifying

  def identifying: Actor.Receive = {
    case ActorIdentity(`path`, Some(actor)) =>
      actor ! SendMail("MySubject","MyBody","FROMPerson","ToPerson")
      context.system.shutdown()

    case ActorIdentity(`path`, None) => println(s"Remote actor not available: $path")
    case _                           => println("Not ready yet")
  }
}

The main application that creates this actor will give it a path to the creator which we will see in a minute. It then sends an Identify message which is built into the Actor system. Once it receives that ActorIdentity (from the Creator) it will send it a SendMail work request and then close off.

In EmailerLookupApplication.scala we start the whole system in a different terminal which starts up an instance of LookupEmailerCreator:

import com.typesafe.config.ConfigFactory  
import akka.actor.ActorSystem  
import akka.actor.Props

object EmailerLookupApplication {  
  def main(args: Array[String]): Unit = {
    val system = ActorSystem("LookupSystem", ConfigFactory.load("remotelookup"))
    val remotePath = "akka.tcp://EmailerCreat[email protected]:2552/user/emailerCreator"
    val actor = system.actorOf(Props(classOf[LookupEmailerCreator], remotePath), "emailerCreator")
  }
}

Running The Lookup

Running the lookup system in a different terminal should start up quickly, fire off the SendMail message and then close down.

You'll notice though that 5 seconds later, the original terminal will have spouted out:
Email Sent with Parameters: MySubject MyBody FROMPerson ToPerson

What's really nice is that a lot of the plumbing is done for you using the default Actors. Interprocess communication is done via netty and all you really have to focus on is defining your work and architecting your actors well.

A few notes to keep in mind though. The system is hard coded but should really be done through the configuration file. Using TypeSafe's ConfigFactory package makes defining arbitrary json objects in the .conf files easy to fetch.

The workload here is pretty light so we can go ahead and call the lookup system many times. If the work to be processed is a bit more resource intensive, you can consider increasing the JVM memory space, building an Akka cluster or having a set amount of workers 'pull' the work to name a few.

It's definitely worth having a play. A million possibilities raced through my head when I first saw what it can do. I hope it inspires you to continue to build fantastic software as well.

Cheers.

comments powered by Disqus