-
Notifications
You must be signed in to change notification settings - Fork 6
Composing
#Composing Akka components
Scala calls itself scalable, because it brings--amongst many other things--mixin composition, allowing you to have functional components (functional in sense of containing some functionality) and then mixin these components together to get the desired application. Let's take a look at how we apply this rather abstract statement in real code.
The ultimate goal is to have testable application with sharply defined components that we then bring together in some main
function or in some Specs2specification. We wish to have code such as
// this defines the entire application with
// * Core actors,
// * Api actors and the
// * Web server
class Application(val actorSystem: ActorSystem) extends Core with Api with Web
// this is our test case that tests the Core actors with the APIs,
// but we don't need the web server for that
class IntegrationSpec extends Specification with SprayTest
with Core with Api with DefaultMarshallers {
"we can make API requests" in {
// some clever code
}
}
##Main
So, how do we go about assembling this beast; what are the Core
, Api
and Web
components and how do they fit together to let us assemble a running application? The names of the traits hint at type of things the actors in those components will be dealing with:
- The core component contains actors that work independently of any API or user interface
- The api component contains actors that translate the [HTTP] requests and responses into objects that the core actors deal with
- The web component exposes the api actors behind a HTTP[s] server
Obviously, the components are traits, and we express functional dependencies between the traits using self-type annotations. Turning to code, we have:
trait Core {
implicit def actorSystem: ActorSystem
implicit val timeout = Timeout(30000)
// code to create the hierarchy of "core" actors
// these are plain akka actors
}
trait Api {
this: Core =>
// ultimately contains the RootService actor that
// provides HTTP processing functionality, using the
// actorSystem from Core
}
trait Web {
this: Api with Core =>
// start the HTTP web server with the
// RootService actor from Api, using the
// actorSystem from Core
}
These self-type annotations allow the compiler to detect malformed application, for example class Application(val actorSystem: ActorSystem) extends Api with Web
: it is missing the Core
actors--how could the Api
work?
###The details
Let's flesh out the details of the code in the components, starting with Core
(though I shall leave the implementation of SpringContextActor
and ApplicationActor
as exercise for curious readers).
trait Core {
implicit def actorSystem: ActorSystem
implicit val timeout = Timeout(30000)
val spring = actorSystem.actorOf(
props = Props[SpringContextActor],
name = "spring")
val application = actorSystem.actorOf(
props = Props[ApplicationActor],
name = "application"
)
Await.ready(spring ? Start(), timeout.duration)
Await.ready(application ? Start(), timeout.duration)
}
Onwards to the Api
. As I said earlier, this component wraps the underlying core actors in code that transforms the requests into messages that the core actors can process and it transforms the messages the core actors may send as replies into API responses.
trait Api {
this: Core =>
val routes =
new UserService().route ::
new AccountService().route ::
Nil
val svc: Route => ActorRef =
route => actorSystem.actorOf(Props(new HttpService(route)))
val root = actorSystem.actorOf(Props(
new RootService(svc(routes.head), routes.tail.map(svc(_)): _*)))
}
Again, I leave the innards of UserService
and AccountService
as exercise; the important point is that we create a List
of the various Route
s that we then use to create Spray's RootService
.
And the ActorRef
to the RootService
is precisely what the Web
component needs to wrap it in a real HTTP server. In code, the whole business is surprisingly easy:
trait Web {
this: Api with Core =>
// every spray-can HttpServer (and HttpClient) needs an IoWorker for
// low-level network IO
// (but several servers and/or clients can share one)
val ioWorker = new IoWorker(actorSystem).start()
// create and start the spray-can HttpServer, telling it that we want
// requests to be handled by the root service actor
val sprayCanServer = actorSystem.actorOf(
Props(new HttpServer(ioWorker,
MessageHandlerDispatch.SingletonHandler(
actorSystem.actorOf(Props(new SprayCanRootService(root)))))),
name = "http-server"
)
// a running HttpServer can be bound, unbound and rebound
// initially to need to tell it where to bind to
sprayCanServer ! HttpServer.Bind("localhost", 8080)
// finally we drop the api thread but hook the shutdown of
// our IoWorker into the shutdown of the applications ActorSystem
actorSystem.registerOnTermination {
ioWorker.stop()
}
}
And that's it. Depending on what components we mixin, we will get the required functionality in our application.
##Testing
If you structure the components of your application this way, testing will become a pleasure: if you want to test the core actors, you mixin just the Core
trait into your tests; if you want to test nearly the entire application, you mixin Core with Api
. Let me show you API test without the web server.
class UserServiceSpec extends Specification with SprayTest
with Core with Api with DefaultMarshallers {
"generate username" in {
testRoute(HttpRequest(GET, "/users/makeUsername"))(root).
response.content.as[String] must_== "generated-username"
}
}
The only addition to SprayTest
is the testRoute
method, which I've added to Spray's code and sent a pull request. If you're eager to try it out now, here it is in its entirety:
trait SprayTest {
...
def testRoute(request: HttpRequest,
timeout: Duration = Duration(10000, TimeUnit.MILLISECONDS))
(root: ActorRef): ServiceResultWrapper = {
val routeResult = new RouteResult
root !
RequestContext(
request = request,
responder = routeResult.requestResponder,
unmatchedPath = request.path
)
// since the route might detach we block until the route actually
// completes or times out
routeResult.awaitResult(timeout)
new ServiceResultWrapper(routeResult, timeout)
}
...
}
#Summary By properly structuring the components in your akka and Spray applications, you will gain freedom in creating instances of your application with just the desired functionality and the [integration] tests will become so much less painful!