如何在Actor

时间:2017-05-02 13:55:42

标签: scala akka akka-http spray-json

我试图编写一个调用HTTP REST API的actor。其余的API需要一个将从调用Actor传递的查询参数。 official documentation有一个例子来使用preStart方法实现上述目的,该方法将消息传递给自己:

import akka.actor.{ Actor, ActorLogging }
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.stream.{ ActorMaterializer, ActorMaterializerSettings }
import akka.util.ByteString

class Myself extends Actor
  with ActorLogging {

  import akka.pattern.pipe
  import context.dispatcher

  final implicit val materializer: ActorMaterializer = ActorMaterializer(ActorMaterializerSettings(context.system))

  val http = Http(context.system)

  override def preStart() = {
    http.singleRequest(HttpRequest(uri = "http://akka.io"))
      .pipeTo(self)
  }

  def receive = {
    case HttpResponse(StatusCodes.OK, headers, entity, _) =>
      entity.dataBytes.runFold(ByteString(""))(_ ++ _).foreach { body =>
        log.info("Got response, body: " + body.utf8String)
      }
    case resp @ HttpResponse(code, _, _, _) =>
      log.info("Request failed, response code: " + code)
      resp.discardEntityBytes()
  }

}

以上工作但URL是硬编码的。我想要实现的是一个REST客户端actor,我可以将参数作为消息发送到并获取调用结果。我修改了上面的代码以接收参数作为消息(伪代码):

      def receive = {

case param: RESTAPIParameter => {
            http.singleRequest(HttpRequest(URI("http://my-rest-url").withQuery("name", "value"))
                .pipeTo(self)
          }

        case HttpResponse(StatusCodes.OK, headers, entity, _) =>
          entity.dataBytes.runFold(ByteString(""))(_ ++ _).foreach { body =>
            log.info("Got response, body: " + body.utf8String)
            sender! body.utf8String    //Will not work
          }
        case resp @ HttpResponse(code, _, _, _) =>
          log.info("Request failed, response code: " + code)
          resp.discardEntityBytes()
      }

以上内容应该可以使用,但是当REST调用的结果通过管道传送回sender时,self引用会丢失,因此无法真正用于将响应发送回客户端。

我想我可以尝试将发件人本地存储在变量中并使用它来传回响应,但我认为这不是一个好主意。

那么,处理这种情况的正确方法是什么?

修改:@ PH88下面建议的解决方案有效,但我希望在外部循环中保持HttpResponse上的模式匹配。

编辑2 :我想将响应传回self的原因是因为我想实现一个状态机.kind of。状态根据actor接收的消息类型而改变。举个例子:

  1. 第一个状态可能是从调用actor接收查询字符串。 actor调用REST api和becomes awaitingResult。数据通过管道进行进一步处理。
  2. 当收到带有成功代码的HTTPResponse时,状态为becomes dataRecevied。数据再次通过管道传输到self以进行更多处理。
  3. 然后将收到的数据转换为内部供应商中立格式,最后将结果发送回主叫参与者。
  4. 如果上面的1中的响应代码不成功,则可以将状态更改为HttpError并进行相应处理。
  5. 希望澄清意图。欢迎任何其他建议/设计实现干净/简单的设计: - )

2 个答案:

答案 0 :(得分:1)

您可以使用自己的案例类封装HttpResponse并将发件人与其捆绑在一起:

case class ServerResponse(requester: ActorRef, resp: HttpResponse)

然后:

  def receive = {

    case param: RESTAPIParameter => {
        val requester = sender
        http.singleRequest(HttpRequest(URI("http://my-rest-url").withQuery("name", "value"))
            .map(httpResp =>
               // This will execute in some other thread, thus
               // it's important to NOT use sender directly 
               ServerResponse(requester, httpResp)
            )
            .pipeTo(self)
      }

    case ServerResponse(requester, HttpResponse(...)) =>
      val result = ...
      requester ! result

    ...
  }

答案 1 :(得分:0)

pipeTo方法将发送方作为隐式参数:

def pipeTo(recipient: ActorRef)(implicit sender: ActorRef = Actor.noSender): Future[T]

Within an Actor self被定义为隐式ActorRef

implicit final val self: ActorRef    

因此self是在pipeTo中指定的发件人。

您只需将发件人明确指定为原始发件人即可:

def receive = {

  case param: RESTAPIParameter => 
     http.singleRequest(HttpRequest(URI("http://my-rest-url").withQuery("name", "value"))
         .pipeTo(self)(sender)  //specify original sender

  case HttpResponse(StatusCodes.OK, headers, entity, _) =>
    entity.dataBytes.runFold(ByteString(""))(_ ++ _).foreach { body =>
      log.info("Got response, body: " + body.utf8String)
      sender! body.utf8String    //Works now!
    }

  case resp @ HttpResponse(code, _, _, _) => {
    log.info("Request failed, response code: " + code)
    resp.discardEntityBytes()
  }
}