4
votes

I'm trying to create an Akka system which would among other things respond to HTTP requests. I created a few actors who exchange messages fine. I can also use akka-http to respond to HTTP requests. The problem is in connecting those two parts.

TL;DR: How to talk to Akka actors during akka-http request processing?

I created a single actor responsible for bringing up the HTTP system:

class HttpActor extends Actor with ActorLogging  {
  /* implicits elided */

  private def initHttp() = {
    val route: Route =  path("status") { get { complete { "OK" } } }
    Http()(context.system).bindAndHandle(route, "localhost", 8080)
  }
  private var bound: Option[Future[Http.ServerBinding]] = None

  override def receive = {
    case HttpActor.Init =>
      bound match {
        case Some(x) => log.warning("Http already bootstrapping")
        case None =>
          bound = Some(initHttp(watcher))
      }

  }
}

object HttpActor {
  case object Init
}

As you may see, the actor creates the akka-http service on the first message it receives (no reason, really, it could do it in the constructor as well).

Now, during the request processing I need to communicate with some other actors, and I can't get it working.

My approach:

  private def initInteractiveHttp() = {
    val route: Route =  path("status") { 
      get { complete { "OK" } } 
    } ~ path("ask") {
      get { complete {
        // Here are the interesting two lines:
        val otherActorResponse = someOtherActor ? SomeMessage
        otherActorResponse.mapTo[String]
    } }
    Http()(context.system).bindAndHandle(route, "localhost", 8080)
  }

This would send a SomeMessage to someOtherActor and wait for response prior to completing the Request-Response cycle. As I understand, however, that messages would be sent from the root HttpActor, which is bad and leads nowhere in terms of scalability. Ideally I would create a distinct instance of specialized actor for every request, but this fails due to akka-http typing. Consider the following example:

class DisposableActor(httpContext: HttpContext) {
    override def preStart() = {
       // ... send some questions to other actors
    }
    override def receive = {
      case GotAllData(x) => httpContext.complete(x)
    }
}

class HttpActorWithDisposables {
  // there is a `context` value in scope - we're an actor, after all
  private def initHttpWithDisposableActors() = {
    val route: Route =  path("status") { 
      get { complete { "OK" } } 
    } ~ path("ask") {
      get { httpContext =>
        val props = Props(new DisposableActor(httpContext))
        val disposableActor = context.actorOf(props, "disposable-actor-name")
        // I have nothing to return here
      }
    }
    Http()(context.system).bindAndHandle(route, "localhost", 8080)
  }

With this approach, I can force the DisposableActor to call httpContext.complete at some point of time. This should correctly end the Request-Response processing cycle. The Route DSL, however, requires to return a valid response (or Future) inside the get block, so this approach doesn't work.

1

1 Answers

4
votes

Your first approach is quite alright, actually. The ask pattern creates a lightweight, disposable actor for you that waits on the result (non-blockingly) to complete the future. It basically does all the things you want to replicate with the DisposableActor and your main HttpActor is not stressed by this.

If you still want to use a different actor, there is completeWith:

completeWith(instanceOf[String]) { complete =>
  // complete is of type String => Unit
  val props = Props(new DisposableActor(complete))
  val disposableActor = context.actorOf(props, "disposable-actor-name")

  // completeWith expects you to return unit
}

And in you actor, call the complete function when you have the result

class DisposableActor(complete: String => Unit) {
  override def receive = {
    case GotAllData(x) => 
      complete(x)
      context stop self // don't for get to stop the actor
  }
}