Typesafe Activator

Reactive Snake - Akka Actor and Web Socket Example

Reactive Snake - Akka Actor and Web Socket Example

stanska
Source
June 7, 2014
akka websocket scaladays2014 play scala

This template demonstrates use of actors and websockets in play, using the famous snake game as an example.

How to get "Reactive Snake - Akka Actor and Web Socket Example" on your computer

There are several ways to get this template.

Option 1: Choose snakeyard in the Typesafe Activator UI.

Already have Typesafe Activator (get it here)? Launch the UI then search for snakeyard in the list of templates.

Option 2: Download the snakeyard project as a zip archive

If you haven't installed Activator, you can get the code by downloading the template bundle for snakeyard.

  1. Download the Template Bundle for "Reactive Snake - Akka Actor and Web Socket Example"
  2. Extract the downloaded zip file to your system
  3. The bundle includes a small bootstrap script that can start Activator. To start Typesafe Activator's UI:

    In your File Explorer, navigate into the directory that the template was extracted to, right-click on the file named "activator.bat", then select "Open", and if prompted with a warning, click to continue:

    Or from a command line:

     C:\Users\typesafe\snakeyard> activator ui 
    This will start Typesafe Activator and open this template in your browser.

Option 3: Create a snakeyard project from the command line

If you have Typesafe Activator, use its command line mode to create a new project from this template. Type activator new PROJECTNAME snakeyard on the command line.

Option 4: View the template source

The creator of this template maintains it at https://github.com/stanska/snakeyard#master.

Option 5: Preview the tutorial below

We've included the text of this template's tutorial below, but it may work better if you view it inside Activator on your computer. Activator tutorials are often designed to be interactive.

Preview the tutorial

Reactive Snake Activator Template

This template uses the popular snake game as an inbrowser app to demonstrate Akka actors using web sockets. Actor Model gives a very natural representation of domain logic by resembling real objects and its interactions. I think this makes the code clearer and easy to read. SnakePool represents a bundle of snakes, it can create snakes and upon game over kill them all. Snake represents a single snake, having cell coordinates as an internal state. Snake can be started, moved (Up, Down, Left or Right) or eat apples. All snakes strive for the one apple. Currently Apple can be created or is eaten. When apple is eaten, a new one is created. The application can be easily extended with more apples, since Apple is also an actor. Snakes and apple need to send data to browser via web socket. In order to have web socket writing encapsulated we have WebSocketChannel which handles Send messages. WebSocketChannel actor helps unit test verification for Snake and Apple unit tests.
All browser requests are handled by SnakeController.
More than one snake pool can run concurrently, if you want to see the power of concurrency and actors, you may test application on two browser instances.

Actors

The Actor Model represents objects and their interactions, resembling human organisations. Actors have addresses on which messages can be send by tell or ! method. Every Actor class must inherit akka.actor.Actor trait and implement receive method where message handling happens.

class SnakePool(webSocketChannel: ActorRef) extends Actor {
  ...
  def receive = {
	...
  }
}
	
Creating Actors

context.actorOf(Snake.props(apple, snakeName, webSocketChannel), snakeName)
	
Method actorOf accepts Akka.props but parameters to actor's constructor can be passed easily by defining a helper function in actor's companion object:

object SnakePool {
  def props(webSocketChannel: ActorRef): Props = Props(new SnakePool(webSocketChannel))
}
	
Then actor creation looks like this:

object SnakeController extends Controller {
  ...
  def startGame = {
	...
	val webSocketChannel = Akka.system.actorOf(WebSocketChannel.props(channel))
	val snakePool = Akka.system.actorOf(SnakePool.props(webSocketChannel))
  ...
}
	
Stopping Actors

There are several ways to stop an actor. In the example application asynchronous stop is used by sending PoisonPill message. The following line of code show how snake dies when eats its tale

self ! PoisonPill

Note that all snakes and the pool die when at least one snake dies. This is covered by SnakePool supervisor who listens for Terminated child messages after register snakes with DeathWatch.

case NewSnake(snakeName) => {
  val snake = context.actorOf(Snake.props(apple, snakeName, webSocketChannel), snakeName)
  snake ! Start
  snakes += (snakeName -> snake)
  context.watch(snake)
}
case t: Terminated => self ! PoisonPill
	
The postStop hook is invoked after an actor is fully stopped. This enables cleaning up of resources. In the example application SnakePool has reference to all snakes in the pool and upon game over, pool is removed and it cleans all snakes corresponding to this pool.

class SnakePool(webSocketChannel: ActorRef) extends Actor {
  var snakes = Map.empty[String, ActorRef]
  ...
  override def postStop() {
	snakes.map(snakeByName => snakeByName._2 ! Kill)
  }
  ...
}
	
Supervision

Supervision describes a dependency relationship between actors: the supervisor delegates tasks to subordinates and therefore must respond to their failures. Here you can find information about supervision. The following code will stop all snakes in case snake throws unhandled exception.


class SnakePool(webSocketChannel: ActorRef) extends Actor {
	override val supervisorStrategy = AllForOneStrategy() {
		case anyException => Stop //if something happens to one snake, kill them all
}
The AllForOneStrategy is applicable in cases where the ensemble of children has such tight dependencies among them, that a failure of one child affects the function of the others.

Fetch an Actor

actorSelection is used to fetch already created snake pool by name. Its argument is actor path Akka.system.actorSelection("/user/" + snakePoolName)

Sending a message

snakePool ! NewSnake(snakeName)
	
More information on actors

Web Sockets

Web Sockets is a next-generation bidirectional communication technology for web applications which operates over a single socket and is exposed via a JavaScript interface in HTML 5 complaint browsers.

Creating a web socket example

snake.js
function startGame(row, col) {
	 for ( i = 1; i <= row*col; i++) {		  
		 off(i)
	 }     
	 websocket = new WebSocket(wsUri + "startgame");

	 websocket.onopen = function (evt) {
		 onOpen(evt, snake_name)
	 };
	 websocket.onclose = function (evt) {
		 onClose(evt, snake_name)
	 };
	 websocket.onmessage = function (evt) {
		 onMessage(evt, snake_name)
	 };
 }
	

object SnakeController extends Controller {
  ...
  def startGame = WebSocket.using[String] { request =>
	val (out, channel) = Concurrent.broadcast[String]
	val webSocketChannel = Akka.system.actorOf(WebSocketChannel.props(channel))
	val snakePool = Akka.system.actorOf(SnakePool.props(webSocketChannel))
	val in = Iteratee.foreach[String] { msg => if (msg == "GET POOL") channel.push("POOL:" + snakePool.path.name); }
	(in, out)
  }
}
	
In order to have startGame function exposed you need to change routes file accordingly

GET   /startgame         snakeyard.SnakeController.startGame
	
Send and receive messages

Once you get a Web Socket connection with the web server, you can send data from browser to server by calling a send() method, and receive data from server to browser by an onmessage event handler. In the example application when socket is open, browser send get pool command and pool name is returned. Pool is keept for future requests, for example to add or move a snake in the pool.

Send a message to server

snake.js
function onOpen(evt, snake_name) {
	 writeToScreen("CONNECTED");
	 websocket.send("GET POOL");
	 enableSnakesButton();
}
	
Receive a message from client

In relation to the snippet above, when get pool request is sent, pool name is returned

val in = Iteratee.foreach[String] { msg => if (msg == "GET POOL") channel.push("POOL:" + snakePool.path.name); }
	
Send message from server

WebSocketChannel keeps a reference to web socket and sends information when Send message is triggered

class WebSocketChannel(channel: Concurrent.Channel[String]) extends Actor {
	def receive = {
	  case Send(data) => channel.push(data)
	}
}
	
Receive a message from server

When information from server is received onMessage is triggered. In the example application onMessage changes background colour of related cells accordingly.

function onMessage(evt, snake_name) {
	 if (evt.data.lastIndexOf("POOL:", 0) === 0) { snake_pool = evt.data.replace("POOL:","");}
	 else if (evt.data.lastIndexOf("APPLE:", 0) === 0) { apple(evt.data.replace("APPLE:",""));}
	 else if (evt.data == "Game Over") { 
		 writeToScreen("GAME OVER");
		 disableSnakesButton();
	 }
	 else if (evt.data > 0) on(evt.data);
	 else off(Math.abs(evt.data));
 }
	

Testing Actors

Tests can only verify externally observable effects. The example bellow show how the asynchronous messages are verified using remove-controlled actor TestProbe.

class SnakeTest extends TestKit(ActorSystem("SnakeTest"))
  with FunSpec {


  describe("Snake Actor Test") {
	it("given new snake when started starts to the right, from thee first cell on the first line") {
	  val apple = TestProbe()
	  val channel = TestProbe()
	  val snakeName = "testSnake1"
	  val snake = system.actorOf(Snake.props(apple.ref, snakeName, channel.ref), snakeName)
	  //when
	  snake ! Start
	  //then
	  apple.expectMsg(Eat(1))
	  apple.expectMsg(Eat(2))
	}
	...
}
	
Using multiple test probes, you can verify conversation content and order of multiple actors. The second example uses mockito mock object and test probe. It shows you how you can inject mocked behaviour in an actor. In order to control what numbers are random in the test, random instance is passed to Apple actor. In a test environment instead of real instance it can be mocked.

class AppleTest extends TestKit(ActorSystem("AppleTest"))
  with FunSpec
  with Mockito {

  describe("Apple Test") {
  ...
 it("When Eat coordinate != apple, then StayHungry") {
	  val client = TestProbe()
	  val channel = TestProbe()
	  val appleName = "testApple3"
	  val random = mock[Random]
	  random.nextInt(SnakePoolConfig.row * SnakePoolConfig.col - 1) returns 123
	  val apple = system.actorOf(Apple.props(random, channel.ref), appleName)


	  //when
	  apple ! NewApple
	  client.send(apple, Eat(135)) 
	  //then
	  client.expectMsg(StayHungry)
    }
  }
}
	
You can find more information on testing actors here
comments powered by Disqus