Completing request outside of main controller in Akka-http

562 views Asked by At

I am pretty new to the Akka world and I have to migrate a project from Spray to akka-http.

In spray, a route was of type Route = RequestContext ⇒ Unit.
But in akka-http, it is of type Route = RequestContext ⇒ Future[RouteResult].

So in spray, we would often handle and complete our requests through a chain of Actors (outside of the main controller) using only fire-and-forget so we didn't have to "ask" and the performance was great. Now, we have to use "ask" every time we pass the request to another Actor (correct me if I'm wrong)

I've been searching a lot and I found a few options which I'm not sure if they fully satisfy me (the need to complete a request in another Actor without the need to return it back to the controller). So that's where you could help me :)

Option 1: Using onSuccess/onComplete
Does using this block my main controller from receiving more requests?

Option 2: Using Futures and using RouteResult.Complete

I've found the following example at How to complete a request in another actor when using akka-http:

import akka.actor.{ActorSystem, Actor, Props}
import akka.pattern.ask
import akka.stream.ActorMaterializer
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{RequestContext, RouteResult}
import scala.concurrent.Future
import akka.http.scaladsl.model.HttpResponse
import akka.util.Timeout

class RequestActor extends Actor {

  //business logic - returns empty HttpResponse
  def handleRequestMessage(requestContext : RequestContext) = 
    RouteResult.Complete(new HttpResponse())

  override def receive = {
    case reqContext : RequestContext => 
      sender ! handleRequestMessage(reqContext)
  }
}//end class RequestActor

object RouteActorTest extends App {
  implicit val as = ActorSystem("RouteActorTest")
  implicit val timeout = new Timeout(1000)

  val actorRef = as actorOf Props[RequestActor]

  def handleRequest(reqContext : RequestContext) : Future[RouteResult] =
    ask(actorRef,reqContext).mapTo[RouteResult]      

  val route = path("") { get(handleRequest) }

  //rest of application...

}//end object RouteActorTest

But this actually passes the response back every time to the previous Actor (the sender) until it reaches the main controller. Another thing about this option is that in the code, it says:

/**
 * The result of handling a request.
 *
 * As a user you typically don't create RouteResult instances directly.
 * Instead, use the methods on the [[RequestContext]] to achieve the desired effect.
 */

So I'm not sure if it's a recommended way of doing it.

I've tried using requestContext.complete() in another actor but it doesn't work (no error thrown, simply doesn't send response) Does anybody know what the best way is to implement the previous architecture we had?

Thanks a lot!

1

There are 1 answers

1
Yuval Itzchakov On

the need to complete a request in another Actor without the need to return it back to the controller

You don't need to complete the request in another actor. All you need to do is for the actor that handles the request to send back a result, and you can use complete to signal completion of the future:

case class MyRequest(data: Int)
case class MyResult(data: Int)

class RequestActor extends Actor {
  override def receive: PartialFunction[Any, Unit] = {
    case MyRequest(data) => sender ! MyResult(data + 1)
  }
}

And route:

get {
  path("yuval") {
    import scala.concurrent.ExecutionContext.Implicits.global
    implicit val timeout = Timeout(5 seconds)

    complete {
      val result = (completionActor ? MyRequest(1)).mapTo[MyResult]
      result.map(r => HttpResponse(StatusCodes.OK, entity = s"Result was ${r.data}"))
    }
  }
}

If you want to handle to handcraft the HttpResponse yourself, you can always use context.complete:

get {
  path("yuval") {
    import scala.concurrent.ExecutionContext.Implicits.global
    implicit val timeout = Timeout(5 seconds)
    context => {
      val result = (completionActor ? MyRequest(1)).mapTo[MyResult]
      result.flatMap(r => context.complete(HttpResponse(StatusCodes.OK, entity = s"Result was ${r.data}")))
    }
  }
}