AkkaのHTTPクライアント(v2.0.2)を使用してRESTサービスを利用する必要があります。論理的なアプローチは、多数の同時接続が予想されるため、ホスト接続プールを介してこれを行うことです。Flow
forこれは(HttpRequest, T)
を消費し、(Try[HttpResponse, T)
を返します。 documentation は、リクエストに対する潜在的な異常応答を管理するために任意のタイプT
が必要であることを示しますが、呼び出し元は、返されたT
を処理することになっています。
私の最初の試みは、Int
をT
として使用する以下の関数です。接続が単一のプールを使用することを保証するために、多くの場所から呼び出されます。
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
をどのように使用する必要があるかということです。よりクリーンでより効率的なソリューションはありますか?そして最後に、何かが順不同で到着するかもしれないという私のパラノイアは、実際にはパラノイアではありませんか?
ドキュメントを数回読むまで、最初はこれに少し混乱していました。プールへの単一のリクエストを使用する場合、同じプールを共有している場所がいくつあっても、提供している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の場合)、どの応答がどれであり、それが理想的ではないかを判断する必要があります。これは失敗のケースをカバーしていません。このソリューションでは、識別子が返されるため、どのリクエストが失敗したかがわかります。
それがあなたにとって少し明確になることを願っています。
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の素晴らしい例に感謝します。
これに関する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())
})