Getting started with Scala, Akka and Sbt: the chat example

October 20th, 2011 | Posted by charless in System

Scala is a general purpose programming language designed to express common programming patterns in a concise, elegant, and type-safe way. It smoothly integrates features of object-oriented and functional languages. Scala programs run on the Java VM, are byte code compatible with Java so you can make full use of existing Java libraries or existing application code.

Akka is the platform for the next generation event-driven, scalable and fault-tolerant architectures on the JVM.

Sbt is a build tool for Scala and Java projects that aims to do the basics well.

In this tutorial, I will show you how to quickly setup your environment to build and run the akka-sample-chat, a sample that comes with the akka distribution, that implements a simple chat system.

Requirements

Setting up your local environment

Required environment

charless@linux-bash:~$ WORKDIR=/home/charless/Work/
charless@linux-bash:~$ cd $WORKDIR
charless@linux-bash:~/Work$ JAVA_HOME=/opt/jdk1.6.0_24/
charless@linux-bash:~/Work$ $JAVA_HOME/bin/java -version
java version "1.6.0_24"
Java(TM) SE Runtime Environment (build 1.6.0_24-b07)
Java HotSpot(TM) Server VM (build 19.1-b02, mixed mode)

Install SBT

SBT is a build tool for Scala projects that aims to do the basics well. It requires Java 1.6 or later.

Summary

  1. Download sbt-launch.jar
  2. Create the launcher
  3. Run sbt

Let’s begin by getting the sbt jar:

charless@linux-bash:~$ mkdir -p bin
charless@linux-bash:~$ cd bin
charless@linux-bash:~/bin$ curl -C - -O http://typesafe.artifactoryonline.com/typesafe/ivy-releases/org.scala-tools.sbt/sbt-launch/0.11.0/sbt-launch.jar
(...)

charless@linux-bash:~/bin$ ls
sbt-launch.jar

To create the launcher, create a file named sbt in ~/bin with the content of the following gist

#!/bin/bash

SBT_HOME="$(cd "$(cd "$(dirname "$0")"; pwd -P)"; pwd)"
JAVA_OPTS="-Dfile.encoding=UTF8 -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m"

java $JAVA_OPTS -jar "$SBT_HOME/sbt-launch.jar" "$@"
view raw sbt This Gist is brought to you using Simple Gist Embed.

By using shell commands:

charless@linux-bash:~/bin$ curl -C - -O https://raw.github.com/gist/1298019/55af692619eabc0b3094a7dcbc1172a0042de755/sbt
(...)
charless@linux-bash:~/bin$ chmod ugo+rx sbt

Open a new terminal and type the following command to test sbt (this may take some minutes as the first run downloads all dependencies):

charless@linux-bash:~$ cd /tmp
charless@linux-bash:/tmp$ ~/bin/sbt
(....)

[info] Set current project to default-8c3933 (in build file:/tmp/)

Ctrl-D

charless@linux-bash:/tmp$

If you have an error like this one

:: problems summary ::
:::: WARNINGS
[NOT FOUND  ] commons-codec#commons-codec;1.2!commons-codec.jar (0ms)
==== Maven2 Local: tried
file:///home/charless/.m2/repository/commons-codec/commons-codec/1.2/commons-codec-1.2.jar

Make sure to delete the directory of the uncomplete artifact from your maven local directory (“~/.m2/path/to/uncomplete/artifact”) and the”~/.ivy2″ directory; restart then the “sbt” command.

For the example:

rm -rf /home/charless/.m2/repository/commons-codec/commons-codec/1.2
rm -rf ~/.ivy2

 

Create the chat application

Summary

  1. Create the project akka-chat-sample
  2. Run the chat application with  sbt console

By default, sbt works purely by convention. sbt will find the following automatically:

  • - Sources in the base directory
  • - Sources in src/main/scala or src/main/java
  • - Tests insrc/test/scala or src/test/java
  • - Data files in src/main/resources or src/test/resources
  • - jars in lib

You can run the project with sbt run or enter the Scala REPL with sbt console. sbt console sets up your project’s classpath so you can try out live Scala examples based on your project’s code.

For our example, we will build the following project in $WORKDIR/akka-sample-chat:

  • build.sbt
  • src/main/scala/ChatServer.scala

First, prepare the project:

charless@linux-bash:~/Work$ mkdir akka-sample-chat
charless@linux-bash:~/Work$ cd akka-sample-chat/
charless@linux-bash:~/Work/akka-sample-chat$ mkdir -p src/main/scala

Create a file named build.sbt in the $WORKDIR/akka-sample-chat directory with the content of the following gist

name := "AkkaSampleChat"

version := "1.0"

scalaVersion := "2.9.1"

resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"

libraryDependencies ++= Seq(
  "se.scalablesolutions.akka" % "akka-actor" % "1.2",
  "se.scalablesolutions.akka" % "akka-stm" % "1.2",
  "se.scalablesolutions.akka" % "akka-remote" % "1.2"
)
view raw build.sbt This Gist is brought to you using Simple Gist Embed.

Then, create a file named ChatServer.scala in the $WORKDIR/akka-sample-chat/src/main/scala directory with the content of the following gist

  /**
* Copyright (C) 2009-2010 Scalable Solutions AB <http://scalablesolutions.se>.
*/

  package sample.chat

  import scala.collection.mutable.HashMap

  import akka.actor.{Actor, ActorRef}
  import akka.stm._
  import akka.config.Supervision.{OneForOneStrategy,Permanent}
  import Actor._
  import akka.event.EventHandler

  /******************************************************************************
Akka Chat Client/Server Sample Application
How to run the sample:

1. Fire up two shells. For each of them:
- Step down into to the root of the Akka distribution.
- Set 'export AKKA_HOME=<root of distribution>.
- Run 'sbt console' to start up a REPL (interpreter).
2. In the first REPL you get execute:
- scala> import sample.chat._
- scala> import akka.actor.Actor._
- scala> val chatService = actorOf[ChatService].start()
3. In the second REPL you get execute:
- scala> import sample.chat._
- scala> ClientRunner.run
4. See the chat simulation run.
5. Run it again to see full speed after first initialization.
6. In the client REPL, or in a new REPL, you can also create your own client
- scala> import sample.chat._
- scala> val myClient = new ChatClient("<your name>")
- scala> myClient.login
- scala> myClient.post("Can I join?")
- scala> println("CHAT LOG:nt" + myClient.chatLog.log.mkString("nt"))


That’s it. Have fun.

******************************************************************************/

  /**
* ChatServer's internal events.
*/
  sealed trait Event
  case class Login(user: String) extends Event
  case class Logout(user: String) extends Event
  case class GetChatLog(from: String) extends Event
  case class ChatLog(log: List[String]) extends Event
  case class ChatMessage(from: String, message: String) extends Event

  /**
* Chat client.
*/
  class ChatClient(val name: String) {
    val chat = Actor.remote.actorFor("chat:service", "localhost", 2552)

    def login = chat ! Login(name)
    def logout = chat ! Logout(name)
    def post(message: String) = chat ! ChatMessage(name, name + ": " + message)
    def chatLog = (chat !! GetChatLog(name)).as[ChatLog].getOrElse(throw new Exception("Couldn't get the chat log from ChatServer"))
  }

  /**
* Internal chat client session.
*/
  class Session(user: String, storage: ActorRef) extends Actor {
    private val loginTime = System.currentTimeMillis
    private var userLog: List[String] = Nil

    EventHandler.info(this, "New session for user [%s] has been created at [%s]".format(user, loginTime))

    def receive = {
      case msg @ ChatMessage(from, message) =>
        userLog ::= message
        storage ! msg

      case msg @ GetChatLog(_) =>
        storage forward msg
    }
  }

  /**
* Abstraction of chat storage holding the chat log.
*/
  trait ChatStorage extends Actor

  /**
* Memory-backed chat storage implementation.
*/
  class MemoryChatStorage extends ChatStorage {
    self.lifeCycle = Permanent

    private var chatLog = TransactionalVector[Array[Byte]]()

    EventHandler.info(this, "Memory-based chat storage is starting up...")

    def receive = {
      case msg @ ChatMessage(from, message) =>
        EventHandler.debug(this, "New chat message [%s]".format(message))
        atomic { chatLog + message.getBytes("UTF-8") }

      case GetChatLog(_) =>
        val messageList = atomic { chatLog.map(bytes => new String(bytes, "UTF-8")).toList }
        self.reply(ChatLog(messageList))
    }

    override def postRestart(reason: Throwable) {
      chatLog = TransactionalVector()
    }
  }

  /**
* Implements user session management.
* <p/>
* Uses self-type annotation (this: Actor =>) to declare that it needs to be mixed in with an Actor.
*/
  trait SessionManagement { this: Actor =>

    val storage: ActorRef // needs someone to provide the ChatStorage
    val sessions = new HashMap[String, ActorRef]

    protected def sessionManagement: Receive = {
      case Login(username) =>
        EventHandler.info(this, "User [%s] has logged in".format(username))
        val session = actorOf(new Session(username, storage))
        session.start()
        sessions += (username -> session)

      case Logout(username) =>
        EventHandler.info(this, "User [%s] has logged out".format(username))
        val session = sessions(username)
        session.stop()
        sessions -= username
    }

    protected def shutdownSessions() {
      sessions.foreach { case (_, session) => session.stop() }
    }
  }

  /**
* Implements chat management, e.g. chat message dispatch.
* <p/>
* Uses self-type annotation (this: Actor =>) to declare that it needs to be mixed in with an Actor.
*/
  trait ChatManagement { this: Actor =>
    val sessions: HashMap[String, ActorRef] // needs someone to provide the Session map

    protected def chatManagement: Receive = {
      case msg @ ChatMessage(from, _) => getSession(from).foreach(_ ! msg)
      case msg @ GetChatLog(from) => getSession(from).foreach(_ forward msg)
    }

    private def getSession(from: String) : Option[ActorRef] = {
      if (sessions.contains(from))
        Some(sessions(from))
      else {
        EventHandler.info(this, "Session expired for %s".format(from))
        None
      }
    }
  }

  /**
* Creates and links a MemoryChatStorage.
*/
  trait MemoryChatStorageFactory { this: Actor =>
    val storage = Actor.actorOf[MemoryChatStorage]
    this.self.startLink(storage) // starts and links ChatStorage
  }

  /**
* Chat server. Manages sessions and redirects all other messages to the Session for the client.
*/
  trait ChatServer extends Actor {
    self.faultHandler = OneForOneStrategy(List(classOf[Exception]),5, 5000)
    val storage: ActorRef

    EventHandler.info(this, "Chat server is starting up...")

    // actor message handler
    def receive: Receive = sessionManagement orElse chatManagement

    // abstract methods to be defined somewhere else
    protected def chatManagement: Receive
    protected def sessionManagement: Receive
    protected def shutdownSessions()

    override def postStop() {
      EventHandler.info(this, "Chat server is shutting down...")
      shutdownSessions()
      self.unlink(storage)
      storage.stop()
    }
  }

  /**
* Class encapsulating the full Chat Service.
* Start service by invoking:
* <pre>
* val chatService = Actor.actorOf[ChatService].start()
* </pre>
*/
  class ChatService extends
    ChatServer with
    SessionManagement with
    ChatManagement with
    MemoryChatStorageFactory {
    override def preStart() {
      remote.start("localhost", 2552);
      remote.register("chat:service", self) //Register the actor with the specified service id
    }
  }

  /**
* Test runner starting ChatService.
*/
  object ServerRunner {

    def main(args: Array[String]) { ServerRunner.run() }

    def run() {
      actorOf[ChatService].start()
    }
  }

  /**
* Test runner emulating a chat session.
*/
  object ClientRunner {

    def main(args: Array[String]) { ClientRunner.run() }

    def run() {

      val client1 = new ChatClient("jonas")
      client1.login
      val client2 = new ChatClient("patrik")
      client2.login

      client1.post("Hi there")
      println("CHAT LOG:nt" + client1.chatLog.log.mkString("nt"))

      client2.post("Hello")
      println("CHAT LOG:nt" + client2.chatLog.log.mkString("nt"))

      client1.post("Hi again")
      println("CHAT LOG:nt" + client1.chatLog.log.mkString("nt"))

      client1.logout
      client2.logout
    }
  }


By using shell commands:

charless@linux-bash:~/Work/akka-sample-chat$ curl -C - -O https://raw.github.com/gist/1299718/5647388fb11e7db4c19bac46154e073bab395c26/build.sbt
(...)
charless@linux-bash:~/Work/akka-sample-chat$ cd src/main/scala/
charless@linux-bash:~/Work/akka-sample-chat/src/main/scala$ curl -C - -O https://raw.github.com/gist/1299742/db527df6babc07827293f5ffefcd57a47ea8a39b/ChatServer.scala
(...)
charless@linux-bash:~/Work/akka-sample-chat/src/main/scala$ ls
ChatServer.scala
charless@linux-bash:~/Work/akka-sample-chat/src/main/scala$ cd ../../..
charless@linux-bash:~/Work/akka-sample-chat$

Launch the sbt console from the $WORKDIR/akka-sample-chat project directory and type the following commands:

  • import sample.chat._
  • import akka.actor.Actor._
  • val chatService = actorOf[ChatService].start()

To do so, open a new terminal and type the following:

charless@linux-bash:~$ PROJECTDIR=/home/charless/Work/akka-sample-chat/
charless@linux-bash:~$ cd $PROJECTDIR
charless@linux-bash:~/Work/akka-sample-chat$ ~/bin/sbt console
[info] Set current project to AkkaSampleChat (in build file:/home/charless/Work/akka-sample-chat/)
(...)
Welcome to Scala version 2.9.1.final (OpenJDK Server VM, Java 1.6.0_22).

scala› import sample.chat._
import sample.chat._

scala› import akka.actor.Actor._
import akka.actor.Actor._

scala› val chatService = actorOf[ChatService].start()
[INFO]    [10/19/11 11:45 PM] [run-main] [ChatService] Chat server is starting up...
(...)
[INFO]    [10/19/11 11:45 PM] [run-main] [MemoryChatStorage] Memory-based chat storage is starting up...
(...)

scala›

In another terminal, launch the sbt console form the $WORKDIR/akka-sample-chat project directory and run the chat simulation run:

charless@linux-bash:~$ PROJECTDIR=/home/charless/Work/akka-sample-chat/
charless@linux-bash:~$ cd $PROJECTDIR
charless@linux-bash:~/Work/akka-sample-chat$ ~/bin/sbt console
[info] Set current project to AkkaSampleChat (in build file:/home/charless/Work/akka-sample-chat/)
(...)
Welcome to Scala version 2.9.1.final (OpenJDK Server VM, Java 1.6.0_22).

scala› import sample.chat._
import sample.chat._

scala› ClientRunner.run
[GENERIC] [10/19/11 11:57 PM] [RemoteClientStarted(akka.remote.netty.NettyRemoteSupport@acd5d4,localhost/127.0.0.1:2552)]
[GENERIC] [10/19/11 11:57 PM] [RemoteClientConnected(akka.remote.netty.NettyRemoteSupport@acd5d4,localhost/127.0.0.1:2552)]
CHAT LOG:
jonas: Hi there
CHAT LOG:
jonas: Hi there
patrik: Hello
CHAT LOG:
jonas: Hi there
patrik: Hello
jonas: Hi again

scala›
Run it again to see full speed after first initialization, and the chat log growing up.

Conclusion

In this article, we have seen how to install sbt and use it to build and test the akka-sample-chat application.

In a next article, we will see how to work on an akka project by using Eclipse.

 

 

 

 

Share and Enjoy

You can follow any responses to this entry through the RSS 2.0 You can leave a response, or trackback.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>