Hiram Software Blog


Reflections using Spray.io

Last month the team took on building a REST API for a client, and the client asked us to use Scala so they could take possession upon completion. We normally are't dogmatic about programming languages or frameworks, but if it's important to a client then it is important to us.

This post outlines why we moved from Spray to implementing our own Servlet Service that used Akka.

Usually our REST APIs are provided by the underlying framework ‐ Play or Spring for example ‐ but this was the first project we did where there was only a REST API and we were asked to avoid Spring and Play. The client wanted this service to speak HTTP with JSON bodies only. No HTML at all.

We eventually settled on Spray as a good compromise. It was selected by the Akka and Play teams as the basis of the "official" Scala HTTP framework, and in general the documentation seemed to reflect a mature and stable product.

We eventually decided, though, that the Spray's DSL was a detractor for using it and ended up writing out own Servlet. If you're not familiar with Spray, let's say you wanted to wanted to expose a REST endpoint at /api/v1/trades

In other environments, like Play, you would define a route and then map that route to some entrypoint in code. So, in Play it would look like

/api/v1/trades/                com.hiramsoftware.knightcap.forex.listTrades()
/api/v1/trades/:tradeId        com.hiramsoftware.knightcap.forex.getTrade(tradeId : String)

Managing the separate routes file can be annoying, but it's a convention that most understand, and so we manage. You'll notice the :tradeId convention borrowed from Rails that signifies a variable named "tradeId" and we map it to the first argument of the getTrade method.

That's all good.

In Spray you would define the mapping inline with the following (to be precise this is using the spray-routing module):

pathPrefix("trades" / String) {
    tradeId => {
        get
        {
            complete 
            {
                "Response to be implicitly cast as a response based on tradeId"
            }    
        }
    }
} ~ 
path("trades") {
    get {
        complete {
            "Response to be implicitly cast as a response"
        }
      }
    }
}

(This example is illustrative, and I haven't checked if it compiles).

It looks great and "clean", right? It is clean and great for the simple cases. The problem is this DSL is very difficult to maintain, at least from our experience.

First, it's possible to forget the tilde (~) that separates the two routes. There is no compilation error if it is missing, and the result will be the last route takes precedence. This is a tricky problem to diagnose because it's only apparent after you learn exactly what's happening. There's no railing to keep new developers from breaking things. In a moderately-sized application, routes are not nicely listed in place, but instead different components handle their routes separately (at least that's how we handle separation of concerns). Missing a tilde is as likely as missing a semicolon ‐ common, but without a compiler to save you.

To me, I'd rather have a system that is ugly but fails obviously than a beautiful system that hides errors.

Second, the nesting became unworkable for our moderately complex application. Let's say there are optional values, authentication, or partial responses (i.e. sessions!) to write... how does one do this in Spray? Each path variable, query parameter, and cookie value one wants to read requires another level. So, it's typically 6 levels of nesting directives for one request. In practice that could be as many as 12 since many requests are similar or have optional values.

Six levels of nesting isn't "bad," but it's also hard to maintain because in what order should one arrange the directives? Should it be path variables first? Then cookies? What if they are optional sometimes?

Third, the "complete" block sounds great in principle but we found it too often to counter our goals. The complete block is designed to run once so caching is possible and performance is improved. However, sometimes in a REST service the state changes and we require responses to be written outside of the complete block.

Perhaps we weren't smart enough to understand and there is something obvious we're missing, but we were unable to find a way.

Fourth, we found the request object to be lacking conveniences we expect from velvet-lined frameworks... for example, getting and setting cookie data shouldn't require reading and writing headers directly. So, too, should it be possible to get a query parameter without having to feed the raw query string to a parser.

Fifth, we found that our business logic was entangled with the routing logic. The biggest cause of this, we think, is the request object noted above. Our business logic had to know how to pull out parameters or deserialize data, but ideally it wouldn't have to care if the input came from a JSON-encoded message or a forms-based submission.

Spray was written by brilliant people

We took a look at the code underneath Spray. There is no question the people behind the framework are 10x coders. Their code is clean and parts of it are above our heads.

This cuts both ways. On one hand it gives us confidence the framework is well-done and should be reliable for production use. On the other hand, it means we feel incapable of making changes.

In theory we should have been able to drop spray-routing since it caused most of our problems and simply wrap spray-http. In practice, though, we found spray-http offered so little value over Servlets + Akka that we found it quicker to write our own than learn and conform to the spray-http concepts.

Wrapping the Akka Framework inside a Servlet

We can't be the first people to come up with this, but in general we implemented a Servlet Service that dispatched Akka messages. Here is the entire service from the prototype (production code is covered by NDA):

package com.hiramsoft.www

import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse}

import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
import com.hiramsoft.model.HSerializer
import com.hiramsoft.www.model._
import com.typesafe.scalalogging.slf4j.LazyLogging

import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.duration._
import scala.util.{Try, Failure, Success}

/**
 * Created by ihiram on 9/25/14.
 */
abstract class ActorServlet extends HttpServlet with LazyLogging {

  implicit val timeout: Timeout = 5.second // for the actor 'asks'

  implicit var execution : ExecutionContext = null;

  var handler : ActorRef = null;

  def getHandler() : ActorRef;

  def getExecutionContext() : ExecutionContext;

  override def init() : Unit = {
    this.handler = getHandler()
    this.execution = getExecutionContext()

    HSerializer.supportEnum(ECode)
  }

  override def service(req : HttpServletRequest, res : HttpServletResponse) : Unit = {
    val asyncCtx = req.startAsync()

    val hReq = ModelConverter.fromSRequest(req, "/api/v1/")

    (this.handler ? hReq).onComplete {
      result => result match {
        case Success(uncaughtResponse) => uncaughtResponse match {
          case eres: EResponse => {
            val body = HSerializer.toString(eres)
            res.addHeader("Access-Control-Allow-Origin", "*")
            res.setStatus(Integer.parseInt(eres.code.toString))
            res.setContentType("application/json")
            res.setContentLength(body.length)
            res.getWriter.println(body)

            asyncCtx.complete()
          }
          case hres: HResponse => {
            for ((k, v) <- hres.headers) {
              res.setHeader(k, v)
            }
            res.addHeader("Access-Control-Allow-Origin", "*")
            res.setContentType(hres.contentType)
            res.setContentLength(hres.body.length)
            res.getWriter.print(hres.body)

            asyncCtx.complete()
          }
          case redir : HRedirect => {
            logger debug s"Redirecting ${redir}"
            res.setStatus(redir.status)
            res.addHeader("Location", redir.location)
            res.getWriter.println("Location: " + redir.location)
            //res.sendRedirect(redir.location)

            asyncCtx.complete()
          }
          case str: String => {
            res.setContentType("text/plain")
            res.getWriter.print(str)
            asyncCtx.complete()
          }
          case ex: Throwable => {
            logger debug ex.getMessage
            res.setContentType("text/plain")
            res.setStatus(500)
            res.addHeader("Access-Control-Allow-Origin", "*")
            res.getWriter().println(ex.getMessage)
            asyncCtx.complete()
          }
          case _ => {
            res.setContentType("application/json")
            res.getWriter.print("{}")
            asyncCtx.complete()
          }
        }
        case Failure(ex) => {
          ex match {
            case timeout : akka.pattern.AskTimeoutException => {
              res.setStatus(500)
              res.setContentType("text/plain")
              res.getWriter.println("The operation could not be completed in a timely manner")
            }
            case _ => {
              logger info "Unexpected top-level exception while processing a request"
              logger debug ex.getClass.toString
              logger debug ex.getMessage
              res.setContentType("text/plain")
              res.setStatus(500)
              res.getWriter().println("Unexpected: ")
              res.getWriter().println(ex.getMessage)
            }
          }
          asyncCtx.complete()
        }
      }
    }

  }
}

Then to do routing, we simply followed the AKKA patterns of using case statements with re-dispatched messages. We wrap the messages into objects that expose common attributes of an HTTP request like method type, path parameter, and query parameter so they can be used in pattern matching.

Here is another example from the prototype code:

class TradeApiActor extends Actor {

  val maxCharLen = 128

  def receive = {
    case areq : ObjAuthReq => {
      val pParams = areq.hReq.pParams
      val dreq = new TradeApiRequest(
        areq = areq,
        method = areq.hReq.method,
        spId = pParams.get("sp"),
        iId = pParams.get("i")
      )

      self.forward(dreq)
    }
      // list Trades
    case dreq @ TradeApiRequest(_, HReqMethod.GET, Some(spIdStr), None) => {
      val spId = ObjId.fromString(spIdStr.take(128))
      sender() ! ModelConverter.toHRespJson(
        TradeSvc.listTrades(dreq.areq.subsId, spId)
      )
    }

    // create new Trade
    case dreq @ TradeApiRequest(_, HReqMethod.POST, Some(spIdStr), None) => {
      val spId = ObjId.fromString(spIdStr.take(128))
      val subsId = dreq.areq.subsId;

      dreq.areq.hReq.getBodyAs[NewTrade]() match {
        case Success(newIreq) => {

          val newUpdate : SpTradeUpdate = new SpTradeUpdate(
            message = newIreq.message,
            asOfDt = Some(DateTime.now()),
            newStatusColor = StatusColor.Warning,
            messageType = TradeMessageType.Discovered
          )

          val newIToBeforeSave = TradeSvc.newTrade(subsId, spId).copy(
            summaryMessage = newIreq.message,
            updates = Array(newUpdate),
            startDt = Some(DateTime.now()),
            summaryStatusColor = newUpdate.newStatusColor
          )

          val newStatusColor = calcNewColor(newIToBeforeSave)

          val newIToSave = newIToBeforeSave.copy(summaryStatusColor = newStatusColor)

          TradeSvc.updateTrade(subsId, spId, newIToSave.iId, newIToSave) match {
            case Success(newI) => {

              sender() ! ModelConverter.toHRespJson(newI)

              CurrentStatusSvc.addTrade(subsId, spId, newI) // don't need to wait for a response

            }
            case Failure(ex) => {
              sender() ! ModelConverter.toEResp(ex)
            }
          }
        }
        case Failure(ex) => sender() ! ModelConverter.toEResp(ex)
      }
    }
    ...

The basic pattern is the actor either processes the message, forwards it to someone else, or sends the sender() a response message.

The top-level Servlet implementation deals with reading and writing HTTP messages, freeing the Akka Actors to handle business logic.

Is this "better"? For us, yes. It gives us basic routing using pattern matching (a concept well known in Scala), an event-based HTTP server based on Akka that we know and trust, and business logic separated from HTTP logic. The Actors speak in terms of case classes, and the Servlet speaks in terms of Servlet Requests and Responses. We handed this service over to the client who already had experience and familiarity managing Servlet Services like this. They were happy, and we felt more confident with the outcome.

We are working to get permission from the client to wrap up the HTTP library and release on GitHub. For now, we plan to stay away from Spray 1.x and look forward to what the team comes up with in a 2.x release. Again, the people behind Spray are clearly top-quality engineers, but they just may be too smart for my team to deliver results on time.

‐ Hiram

P.S. Some of the complexity, but not all, comes from Spray's desire to support Servlet and non-Servlet environments. In our case, running within a Servlet is more than sufficient since most customers we have and know about run production services in an app server.

No one that we know about sets up a "play run" (or "activator run" as it is now called) script in production.