Playing with Actor Room

In the previous section, we have seen a number of projects that are using Enumerators/Iteratees to send and receive messages reactively, with various levels of complexity. Iteratees are powerful, but using them can sometimes lead to code snippets that are not easy to understand. The Play Actor Room project, which is available at https://github.com/mandubian/play-actor-room, proposes to reduce some of the complexity of setting up Iteratees by abstracting away this part and letting the programmer focus only on the domain logic, such as processing incoming messages and assembling outgoing messages. This project started from the observation that many applications need the same functionality, which can be seen as a server Room (holding state, for instance, and being the middle man between distributed clients). The role of this room is to listen for incoming messages from connected clients, and either broadcast received messages after processing them or just unicast communication to a single client. It is a good illustration of how an application can react to users/events. Typical applications such as a multiuser chat are therefore very straightforward to write, and they are one of the two samples given as examples. Let's experiment with the most basic use of the actor room support, a sample called simplest.

To clone the project somewhere on your disk, just enter the following command:

> git clone https://github.com/mandubian/play-actor-room

First, we can look at the application once it is running:

> cd play-actor-room/samples/simplest
> play run

Opening a browser at the default play port (http://localhost:9000/) will show you a simple sign-in widget, as shown in the following screenshot. Enter your name to log in, type a message in the provided text area, and then press Enter.

Playing with Actor Room

In the console window where you started the actor room application, you should now see the logging information printed by the actor that received messages from the client browser. The information can be seen as follows:

[info] play - Application started (Dev)
[info] play - Starting application default Akka system.
[debug] application - Connected Member with ID:Thomas
[info] application - received Play Actor Room rocks

On opening several browser windows and logging in with different names, you can see all the messages hitting the server room, that is, at the console. The actor room actually broadcasts the received messages back to all connected browsers, although for now there is nothing in the view to handle the messages.

You can, however, open the console of one browser to see the display of the broadcast messages, as shown in the following screenshot:

Playing with Actor Room

Additionally, invoking the http://localhost:9000/list/ URL from a third window will return the list of currently connected clients.

Some of the interesting features of this basic application can be observed once we import the project into eclipse (entering the > play eclipse command) and open the controller that includes the implementation of the receiving Actor class.

The Receiver actor that acts as the server has been created by a supervisor Actor. It handles messages in JSON format. All the default logic of the receiving Actor, which is the only code that we need to care about for processing messages from clients, is as follows:

class Receiver extends Actor {
  def receive = {
    case Received(from, js: JsValue) =>
      (js  "msg").asOpt[String] match {
        case None => play.Logger.error("couldn't msg in websocket event")
        case Some(s) =>
          play.Logger.info(s"received $s")
          context.parent ! Broadcast(from, Json.obj("msg" -> s))
      }
  }
}

Note that broadcasting the response from the server to all the clients is done by the supervising actor referenced by context.parent. In the previous logic, the Broadcast message also includes the originator from ActorRef reference.

As a small modification to the default room behavior to fit new business requirements, we can, for instance, reuse the TravelAgent, Flight, and Hotel actors that we created in Chapter 8, Essential Properties of Modern Applications – Asynchrony and Concurrency. We want to provide each user with the ability to book a flight, and (at any time) monitor how many seats are still available. To do this, we can involve a slightly bigger JSON message as the exchange format between the server and client.

A useful enhancement to Scala that came with Version 2.10 is the notion of string interpolation. We already used this feature throughout this book and introduced it in Chapter 1, Programming Interactively within Your Project. Similarly, JSON interpolation has been created as an extension to the JSON support in Play. We can reuse JSON interpolation, for instance, to do some elegant pattern matching. Just add the following extension dependencies to the Build.scala file:

val appDependencies = Seq(
    "org.mandubian" %% "play-actor-room" % "0.1",
    "play-json-zipper" %% "play-json-zipper" % "1.0",
    "com.typesafe.play" %% "play-json"           % "2.2.0"
  )

Once in place, the JSON pattern matching feature handles the JSON messages coming from the browser client to the Receiver actor, as follows

case Received(from, js: JsValue) =>
      js match {
        case json"""{
          "booking":"flight",
          "numberOfPersons":$v1
        }""" =>  play.Logger.info(s"received $v1")
        …

Let's add a Flight actor to keep the count of seats available. In a new package actors, which is directly under the app/ source directory, we can add a Flight.scala class that looks like the following:

package actors

import akka.actor.Actor
import akka.event.LoggingReceive

object Flight {
  case class BookSeat(number:Int) {
    require(number > 0)
  }
  case object GetSeatsLeft
  case object Done
  case object Failed
}
class Flight extends Actor {
  import Flight._
  
  def book(seats:Int):Receive = LoggingReceive {
    case BookSeat(nb) if nb <= seats =>
      context.become(book(seats-nb))
      sender ! Done
    case GetSeatsLeft => sender ! seats
    case _ => sender ! Failed
  }
  
  def receive = book(50) // Initial number of available seats
}

Rather than creating a mutable state variable var seatsLeft, as we did in Chapter 8, Essential Properties of Modern Applications – Asynchrony and Concurrency, we encapsulated this state change as an argument passed while switching context each time we receive a BookSeat message. This way of proceeding is a recommended best practice to avoid holding mutable variables. We have added a GetSeatsLeft message to be able to query the value of the current state, in which case the state is sent back to the sender actor.

On the client side, we can modify the index.scala.html view to add a couple of simple widgets to our application. In particular, we can add a placeholder to display the number of available seats left in the flight. This is the information that will be pushed to all connected browsers by the server room actor. An example of such a view is as follows:

@(connected: Option[String] = None)

@main(connected) {

  @connected.map { id =>
    <p class="pull-right">
     Logged in as @id
       <a href="@routes.Application.index()">Disconnect</a>
    </p>
    <div>Places left in flight: <input size="10" id="placesLeft"></input></div>
      
    <div>
      <select id ="booking">
        <option value="flight">Flight</option>
        <option value="hotel">Hotel</option>
      </select>
      Number of persons to book:
      <textarea id ="numberOfPersons" ></textarea>
    </div>

    <script type="text/javascript" charset="utf-8" src="@routes.Application.websocketJs(id)"></script>
  }.getOrElse {
    <form action="@routes.Application.connect(None)" class="pull-right">
      <input id="username" name="id" class="input-small" type="text" placeholder="Username">
        <button class="btn" type="submit">Sign in</button>
    </form>
  }
}

We also need to slightly modify the small JavaScript snippet that handles communication between the client browser and the server via the WebSocket so that it handles the new JSON format. The modified websocket.scala.js file is given as follows:

@(id: String)(implicit r: RequestHeader)

$(function() {

  var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket;
  var wsSocket = new WS("@routes.Application.websocket(id).webSocketURL()");
  var sendMessage = function() {
    wsSocket.send(JSON.stringify(
       {
         "booking":$("#booking").val(),
         "numberOfPersons":$("#numberOfPersons").val()
       }
    ))
    $("#numberOfPersons").val(''),
  }

  var receiveEvent = function(event) {
    console.log(event);
    var data = JSON.parse(event.data);
    // Handle errors
    if(data.error) {
      console.log("WS Error ", data.error);
      wsSocket.close();
      // TODO manage error
      return;
    } else {
      console.log("WS received ", data);
      // TODO manage display
      $("#placesLeft").val(data.placesLeft);
    }
  }

  var handleReturnKey = function(e) {
    if(e.charCode == 13 || e.keyCode == 13) {
      e.preventDefault();
      sendMessage();
    }
  }

  $("#numberOfPersons").keypress(handleReturnKey);

  wsSocket.onmessage = receiveEvent;

})

Finally, in the Application.scala file of the server part, we can extend the Receiver actor to handle incoming JSON messages and contact the Flight actor to both update and read the current value of its state, as follows:

[…imports from the original actor room sample…]
import play.api.libs.json._
import play.api.libs.functional.syntax._
import play.api.libs.json.extensions._

import actors._

object Receiver {
  val flightBookingActor = Akka.system.actorOf(Props[Flight],"flight")
}
class Receiver extends Actor {
  import Receiver.flightBookingActor
  
  def receive = LoggingReceive {
    case x:Int => 
      play.Logger.info(s"Received number of seats left: $x")
      val placesLeft:String = if (x<0) "Fully Booked" else x.toString
      context.parent ! Broadcast("flight", Json.obj("placesLeft" -> placesLeft))  
    case Received(from, js: JsValue) =>
      js match {
        case json"""{
          "booking":"flight",
          "numberOfPersons":$v1
        }""" => 
          play.Logger.info(s"received $v1")
          val nbOfPersons = v1.as[String]
          flightBookingActor ! Flight.BookSeat(nbOfPersons.toInt)
          val placesCount = flightBookingActor ! Flight.GetSeatsLeft           
        case _ => play.Logger.info(s"no match found")
      }
  }
}

Now that we have all the pieces in place, let's run the example in a couple of browsers. Notice that we have added the LoggingReceive call to both the Receiver and Flight actors so that we get extensive logging output once we execute the server code. On the command prompt, you may enter the following commands to start the Play application with the additional flags to activate the logging output:

> play
> run -Dakka.loglevel=DEBUG -Dakka.actor.debug.receive=true

Open two browser windows (possibly using two different browsers) at the URL http://localhost/9000. Complete the sign-in step; for instance, use Alice and Bob as names to connect to the actor room from the two browsers, respectively.

Entering the seats that you want to book from either window will update the global number of seats left in both windows, as illustrated in the following screenshot:

Playing with Actor Room

The console output from the server should display the logging information as follows:

[info] play - Starting application default Akka system.
[debug] application - Connected Member with ID:Alice
[debug] application - Connected Member with ID:Bob

Received(Bob,{"booking":"flight","numberOfPersons":"5"})

Received(Alice,{"booking":"flight","numberOfPersons":"3"})

[info] application - Received number of seats left: 42
[DEBUG] [02/15/2014 22:51:01.226] [application-akka.actor.default-dispatcher-7] [akka://application/user/flight] received handled message GetSeatsLeft
[DEBUG] [02/15/2014 22:51:01.226] [application-akka.actor.default-dispatcher-6] [akka://application/user/$a/Alice-receiver] received handled message 42

Entering a number of seats that is greater than the number of remaining places will not update the counter, and it will end up in a Fail message from the Flight actor.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.12.50