3
votes

I have become interested in the akka-http implementation but one thing that strikes me as kind of an anti-pattern is the creation of a DSL for all routing, parsing of parameters, error handling and so on. The examples given in the documents are trivial in the extreme. However I saw a route of a real product on the market and it was a massive 10k line file with mesting many levels deep and a ton of business logic in the route. Real world systems ahave to deal with users passing bad parameters, not having the right permissions and so on so the simple DSL explodes fast in real life. To me the optimal solution would be to hand off the route completion to actors, each with the same api who will then do what is needed to complete the route. This would spread out the logic and enable maintainable code but after hours I have been unable to manage it. With the low level API I can pass off the HttpRequest and handle it the old way but that leaves me out of most of the tools in the DSL. So is there a way I could pass something to an actor that would enable it to continue the DSL at that point, handling route specific stuff? I.e. I am talking about something like this:

  class MySlashHandler() extends Actor {
    def receive = {
      case ctxt: ContextOfSomeKind =>
        decodeRequest {
          // unmarshal with in-scope unmarshaller
          entity(as[Order]) { order =>
            sender ! "Order received"
          }
        context.stop(self)
    }
  }

  val route =
    pathEndOrSingleSlash {
      get { ctxt =>
        val actor = actorSystem.actorOf(Props(classOf[MySlashHandler]))
        complete(actor ? ctxt)
      }
    }

Naturally that wont even compile. Despite my best efforts i haven't found a type for ContextOfSomeKind or how to re-enter the DSL once I am inside the actor. It could be this isnt possible. If not I dont think I like the DSL because it encourages what I would consider horrible programming methodology. Then the only problem with the low level API is getting access to the entity marshallers but I would rather do that then make a massive app in a single source file.

2
I personally have habit of splitting routes into number of traits simply because it's impossible to work in IntelliJ IDEA in single 10k routing file full of implicits. - expert
Even if possible it feels like an anti-pattern to me. - Robert Simmons Jr.
Well, it's question of personal taste I guess :) I prefer readability to strict following of patters. But if you find better way to organize complex akka-http routes I'll be happy to read about it :) - expert

2 Answers

3
votes

Offload Route Completion

Answering your question directly: a Route is nothing more than a function. The definition being:

type Route = (RequestContext) => Future[RouteResult]

Therefore you can simply write a function that does what you are look for, e.g. sends the RequestContext to an Actor and gets back the result:

class MySlashHandler extends Actor {

  val routeHandler = (_ : RequestContext) => complete("Actor Complete")

  override def receive : Receive = {
    case requestContext : RequestContext => routeHandler(requestContext) pipeTo sender
  }
}

val actorRef : ActorRef = actorSystem actorOf (Props[MySlashHandler])

val route : Route = 
  (requestContext : RequestContext) => (actorRef ? requestContext).mapTo[RouteResult]

Actors Don't Solve Your Problem

The problem you are trying to deal with is the complexity of the real world and modelling that complexity in code. I agree this is an issue, but an Actor isn't your solution. There are several reasons to avoid Actors for the design solution you seek:

  1. There are compelling arguments against putting business logic in Actors.
  2. Akka-http uses akka-stream under the hood. Akka stream uses Actors under the hood. Therefore, you are trying to escape a DSL based on composable Actors by using Actors. Water isn't usually the solution for a drowning person...
  3. The akka-http DSL provides a lot of compile time checks that are washed away once you revert to the non-typed receive method of an Actor. You'll get more run time errors, e.g. dead letters, by using Actors.

Route Organization

As stated previously: a Route is a simple function, the building block of scala. Just because you saw an example of a sloppy developer keeping all of the logic in 1 file doesn't mean that is the only solution.

I could write all of my application code in the main method, but that doesn't make it good functional programming design. Similarly, I could write all of my collection logic in a single for-loop, but I usually use filter/map/reduce.

The same organizing principles apply to Routes. Just from the most basic perspective you could break up the Route logic according to method type:

//GetLogic.scala
object GetLogic {
  val getRoute = get {
    complete("get received")
  }  
}

//PutLogic.scala
object PutLogic {
  val putRoute = put {
    complete("put received")
  }
}

Another common organizing principle is to keep your business logic separate from your Route logic:

object BusinessLogic {

  type UserName = String
  type UserId = String     

  //isolated business logic 
  val dbLookup(userId : UserId) : UserName = ???

  val businessRoute = get {
    entity(as[String]) { userId => complete(dbLookup(userId)) } 
  }
}

These can then all be combined in your main method:

val finalRoute : Route = 
  GetLogic.getRoute ~ PutLogic.putRoute ~ BusinessLogic.businessRoute

The routing DSL can be misleading because it sort of looks like magic at times, but underneath it's just plain-old functions which scala can organize and isolate just fine...

2
votes

I encountered a problem like this last week as well. Eventually I ended up at this blog and decided to go the same way as described there.

I created a custom directive which makes it possible for me to pass request contexts to Actors.

   def imperativelyComplete(inner: ImperativeRequestContext => Unit): Route = { ctx: RequestContext =>
       val p = Promise[RouteResult]()
       inner(new ImperativeRequestContext(ctx, p))
       p.future
   }

Now I can use this in my Routes file like this:

val route = 
    put {
        imperativelyComplete { ctx =>
            val actor = actorSystem.actorOf(Props(classOf[RequestHandler], ctx))
            actor ! HandleRequest
        }
    }

With my RequestHandler Actor looking like the following:

class RequestHandler(ctx: ImperativeRequestContext) extends Actor {
    def receive: Receive = {
        case handleRequest: HandleRequest =>
            someActor ! DoSomething() // will return SomethingDone to the sender
        case somethingDone: SomethingDone =>
            ctx.complete("Done handling request")
            context.stop(self)
    }
}

I hope this brings you into the direction of finding a better solution. I am not sure if this solution should be the way to go, but up until now it works out really well for me.