3

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.

Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125
Robert Simmons Jr.
  • 1,182
  • 8
  • 21
  • 1
    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 Apr 03 '17 at 08:13
  • Even if possible it feels like an anti-pattern to me. – Robert Simmons Jr. Apr 03 '17 at 14:41
  • 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 Apr 03 '17 at 18:23

2 Answers2

3

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...

Community
  • 1
  • 1
Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125
  • "Actors shouldn't have business logic and should just be a concurrency layer," you say. I couldn't disagree with that statement more if I tried. I have deployed 1 medium system using actors and another extremely large system that deals with unbelievable amounts of data and it works at unbelievable speed on commodity hardware. If you view actors merely as a concurrency layer then I think either you haven't explored them deeply enough or you are missing the point in other ways. Actors are far more than a future on steroids, they have a lot of capabilities that you just haven't tapped. – Robert Simmons Jr. Apr 03 '17 at 14:40
  • @RobertSimmonsJr. Which Actor capabilities outside of "concurrency layer" are you referring to? You can leverage all of the mailbox queueing, messaging, state machine, etc. without having the business code inside of the Actor class definition. What feature is missed by keeping the business logic in one class, and Actor logic in another? Note, my argument is **not** to avoid using Actors altogether, but to use them the right way... – Ramón J Romero y Vigil Apr 03 '17 at 15:10
  • :-) In only 570 chars? When deploying apps that experience heavy load you can architect systems that leverage message processing paradigm to massively accelerate things. I architected a REAL WORLD business system for DFS that runs on a 9 node commodity cluster and serves terabytes live, rapidly changing, financially perfect of data daily. Would be impossible to explain in SO comment. The actors contain ALL the business logic. You cant just take an Spring MVC style app and port it to actors and expect good results, you have to think differently. Many execs have seen it and are AMAZED. – Robert Simmons Jr. Apr 03 '17 at 15:33
  • I completely agree that the actor model is great for "message processing paradigm to massively accelerate things". How does that translate to the Actor class definition should contain the business logic? If you look at the example link I referenced I was able to use Actors and still keep the logic outside of the Actor code. You keep making a compelling case for using Actors and not a compelling case for sticking your code inside them. There's a difference... – Ramón J Romero y Vigil Apr 03 '17 at 15:38
  • Because the actors and messages are integral to the business logic. The system doesn't just merely call a spring-MVC style "controller" to implement the logic, the actual work is done not by an imperative design but by a message processing design. Like I said, it doesn't lend itself to 570 char comments. There are also NDA constraints I am under of course. Book examples are too trivial, pathetically trivial. This system is a powerhouse. But again, you have to toss out everything you know about back-end server design and start over. That is honestly the hardest part. – Robert Simmons Jr. Apr 03 '17 at 15:49
  • Messages are nothing but immutable data objects. There is almost never any akka "stuff" in a message, they're usually case classes with data. And, again, making Actors "integral to the business logic" simply shows the same lack of organization that you complained about in the original question. At the very heart of **any** receive method is just multiple case statements and the corresponding function to call. There is nothing about those called functions that **have to be** in the Actor itself... – Ramón J Romero y Vigil Apr 03 '17 at 15:53
  • Pretty bold statement since you haven't seen the code. But I really have to do some work now. Good luck! – Robert Simmons Jr. Apr 03 '17 at 16:03
  • To you as well sir. – Ramón J Romero y Vigil Apr 03 '17 at 16:04
2

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.

Frank Levering
  • 401
  • 3
  • 16