在使用期货,Akka流和Akka参与者的并发环境中,与非线程安全服务集成,同时保持背压

时间:2019-02-15 14:39:29

标签: scala akka akka-stream

我正在使用第三方库提供解析服务(在我的情况下为用户代理解析),该服务不是线程安全的库,必须在单线程基础上运行。我想编写一个线程安全的API,该线程安全的API可以被多个线程调用以通过Futures API与之交互,因为该库可能会引入一些潜在的阻塞(IO)。我还想在必要时提供反压力,并在解析器无法赶上生产者的情况下返回失败的未来。

这实际上可能是一个通用的要求/问题,如何与不是线程安全的任何客户端/库(用户代理/地理位置解析器,redis之类的db客户端,fluentd之类的记录收集器)进行交互,并发环境。

我想出了以下公式:

  1. 将解析器封装在专用的Actor中。

  2. 创建一个akka流源队列,该队列接收包含用户代理和要完成的Promise的ParseReuqest,并使用通过mapAsync的ask模式与解析器actor进行交互。

  3. 创建另一个参与者以封装源队列。

这是要走的路吗?还有其他方法可以实现这一目标,也许更简单?也许使用图阶段?不用询问模式和更少的代码就能完成吗?

数字3中提到的actor,是因为我不确定源队列是否是线程安全的?我希望它只是在文档中说明,但事实并非如此。网络上有多个版本,有的说不是,有的说是。

源队列一旦实现,是否可以安全地从不同线程推送元素?

(该代码可能无法编译,容易出现潜在故障,并且仅适用于此问题)

class UserAgentRepo(dbFilePath: String)(implicit actorRefFactory: ActorRefFactory) {

import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
implicit val askTimeout = Timeout(5 seconds)

// API to parser - delegates the request to the back pressure actor
def parse(userAgent: String): Future[Option[UserAgentData]] = {
  val p = Promise[Option[UserAgentData]]
  parserBackPressureProvider ! UserAgentParseRequest(userAgent, p)
  p.future
}

// Actor to provide back pressure that delegates requests to parser actor
private class ParserBackPressureProvider extends Actor {
  private val parser = context.actorOf(Props[UserAgentParserActor])

  val queue = Source.queue[UserAgentParseRequest](100, OverflowStrategy.dropNew)
    .mapAsync(1)(request => (parser ? request.userAgent).mapTo[Option[UserAgentData]].map(_ -> request.p))
    .to(Sink.foreach({
      case (result, promise) => promise.success(result)
    }))
    .run()

  override def receive: Receive = {
    case request: UserAgentParseRequest => queue.offer(request).map {
      case QueueOfferResult.Enqueued =>
      case _ => request.p.failure(new RuntimeException("parser busy"))
    }
  }
}

// Actor parser
private class UserAgentParserActor extends Actor {
  private val up = new UserAgentParser(dbFilePath, true, 50000)
  override def receive: Receive = {
    case userAgent: String =>
      sender ! Try {
      up.parseUa(userAgent)
    }.toOption.map(UserAgentData(userAgent, _))
  }
}

private case class UserAgentParseRequest(userAgent: String, p: Promise[Option[UserAgentData]])

private val parserBackPressureProvider = actorRefFactory.actorOf(Props[ParserBackPressureProvider])

}

1 个答案:

答案 0 :(得分:0)

为此使用演员吗?

似乎您并不需要所有的复杂性,scala / java拥有“现成的”所有需要的工具:

 class ParserFacade(parser: UserAgentParser, val capacity: Int = 100) {
    private implicit val ec = ExecutionContext
      .fromExecutor(
         new ThreadPoolExecutor(
           1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(capacity)
         )
      )

   def parse(ua: String): Future[Option[UserAgentData]] = try {
     Future(Some(UserAgentData(ua, parser.parseUa(ua)))
       .recover { _ => None }
   } catch {
     case _: RejectedExecutionException => 
       Future.failed(new RuntimeException("parser is busy"))
   }
}