web-dev-qa-db-ja.com

Akkahttpクライアント接続プールの正しい使用

AkkaのHTTPクライアント(v2.0.2)を使用してRESTサービスを利用する必要があります。論理的なアプローチは、多数の同時接続が予想されるため、ホスト接続プールを介してこれを行うことです。Flowforこれは(HttpRequest, T)を消費し、(Try[HttpResponse, T)を返します。 documentation は、リクエストに対する潜在的な異常応答を管理するために任意のタイプTが必要であることを示しますが、呼び出し元は、返されたTを処理することになっています。

私の最初の試みは、IntTとして使用する以下の関数です。接続が単一のプールを使用することを保証するために、多くの場所から呼び出されます。

val pool = Http().cachedHostConnectionPool[Int]("127.0.0.1", 8888, ConnectionPoolSettings(system))

def pooledRequest(req: HttpRequest): Future[HttpResponse] = {
  val unique = Random.nextInt
  Source.single(req → unique).via(pool).runWith(Sink.head).flatMap {
    case (Success(r: HttpResponse), `unique`) ⇒ Future.successful(r)
    case (Failure(f), `unique`) ⇒ Future.failed(f)
    case (_, i) ⇒ Future.failed(new Exception("Return does not match the request"))
  }
}

問題は、クライアントがこのTをどのように使用する必要があるかということです。よりクリーンでより効率的なソリューションはありますか?そして最後に、何かが順不同で到着するかもしれないという私のパラノイアは、実際にはパラノイアではありませんか?

15
David Weber

ドキュメントを数回読むまで、最初はこれに少し混乱していました。プールへの単一のリクエストを使用する場合、同じプールを共有している場所がいくつあっても、提供しているT(この場合はInt)は使用しません。案件。したがって、常にSource.singleを使用している場合、本当に必要な場合は、そのキーを常に1にすることができます。

ただし、それが機能するのは、コードの一部がプールを使用し、一度に複数のリクエストをプールに送信し、それらすべてのリクエストからの応答を必要とする場合です。その理由は、応答がプールに提供された順序ではなく、呼び出されたサービスから受信した順序で返されるためです。各リクエストには異なる時間がかかる可能性があるため、プールから受信された順序でSinkにダウンストリームで流れます。

次の形式のURLでGETリクエストを受け入れるサービスがあったとします。

/product/123

ここで、123部分は、検索したい製品のIDです。商品1-10を一度に検索し、それぞれに個別のリクエストを送信したい場合は、ここで識別子が重要になり、各HttpResponseを目的の商品IDと関連付けることができます。このシナリオの簡略化されたコード例は次のとおりです。

val requests = for(id <- 1 until 10) yield (HttpRequest(HttpMethods.GET, s"/product/$id"), id)
val responsesMapFut:Future[Map[Int,HttpResponse]] = 
  Source(requests).
    via(pool).
    runFold(Map.empty[Int,HttpResponse]){
      case (m, (util.Success(resp), id)) => 
        m ++ Map(id -> resp)

      case (m, (util.Failure(ex), i)) =>
        //Log a failure here probably
          m
    }

foldで応答を取得すると、それぞれが関連付けられているIDも便利に用意されているので、idでキー設定されたMapに応答を追加できます。この機能がなければ、おそらく本体を解析して(jsonの場合)、どの応答がどれであり、それが理想的ではないかを判断する必要があります。これは失敗のケースをカバーしていません。このソリューションでは、識別子が返されるため、どのリクエストが失敗したかがわかります。

それがあなたにとって少し明確になることを願っています。

24
cmbaxter

Akka HTTP接続プールは、HTTPベースのリソースを消費する際の強力な味方です。一度に1つのリクエストを実行する場合、解決策は次のとおりです。

def exec(req: HttpRequest): Future[HttpResponse] = {
  Source.single(req → 1)
    .via(pool)
    .runWith(Sink.head).flatMap {
      case (Success(r: HttpResponse), _) ⇒ Future.successful(r)
      case (Failure(f), _) ⇒ Future.failed(f)
    }
}

singleリクエストを実行しているため、レスポンスを明確にする必要はありません。ただし、Akkaストリームは賢いです。複数のリクエストを同時にプールに送信できます。この例では、Iterable[HttpRequest]を渡します。返されたIterable[HttpResponse]は、SortedMapを使用して元のリクエストと同じ順序に並べ替えられます。 request Zip responseを実行するだけで、次のことができます。

def exec(requests: Iterable[HttpRequest]): Future[Iterable[Future[HttpResponse]]] = {
  Source(requests.zipWithIndex.toMap)
    .via(pool)
    .runFold(SortedMap[Int, Future[HttpResponse]]()) {
      case (m, (Success(r), idx)) ⇒ m + (idx → Future.successful(r))
      case (m, (Failure(e), idx)) ⇒ m + (idx → Future.failed(e))
    }.map(r ⇒ r.values)
}

物事を自分のやり方で開梱する必要がある場合、反復可能な先物の先物は素晴らしいです。物を平らにするだけで、より簡単な応答が得られます。

def execFlatten(requests: Iterable[HttpRequest]): Future[Iterable[HttpResponse]] = {
  Source(requests.zipWithIndex.toMap)
    .via(pool)
    .runFold(SortedMap[Int, Future[HttpResponse]]()) {
      case (m, (Success(r), idx)) ⇒ m + (idx → Future.successful(r))
      case (m, (Failure(e), idx)) ⇒ m + (idx → Future.failed(e))
    }.flatMap(r ⇒ Future.sequence(r.values))
}

HTTPサービスを利用するためのクライアントを作成するために、すべてのインポートとラッパーを使用して this Gist を作成しました。

@cmbaxterの素晴らしい例に感謝します。

7
David Weber

これに関するakka-httpドキュメントを改善するためのオープンチケットがあります。お願いします この例を確認してください

val pool = Http().cachedHostConnectionPool[Promise[HttpResponse]](Host = "google.com", port = 80)
val queue = Source.queue[(HttpRequest, Promise[HttpResponse])](10, OverflowStrategy.dropNew)
  .via(pool)
  .toMat(Sink.foreach({
     case ((Success(resp), p)) => p.success(resp)
    case ((Failure(e), p)) => p.failure(e)
  }))(Keep.left)
  .run


val promise = Promise[HttpResponse]
val request = HttpRequest(uri = "/") -> promise

val response = queue.offer(request).flatMap(buffered => {
  if (buffered) promise.future
  else Future.failed(new RuntimeException())
})
0
jfuentes