Host-Level Client-Side API
As opposed to the Connection-Level Client-Side API the host-level API relieves you from manually managing individual HTTP connections. It autonomously manages a configurable pool of connections to one particular target endpoint (i.e. host/port combination).
Requesting a Host Connection Pool
The best way to get a hold of a connection pool to a given target endpoint is the
method, which returns a
Flow that can be "baked" into an application-level stream setup. This flow is also called
a "pool client flow".
The connection pool underlying a pool client flow is cached. For every
ActorSystem, target endpoint and pool
configuration there will never be more than a single pool live at any time.
Also, the HTTP layer transparently manages idle shutdown and restarting of connection pools as configured. The client flow instances therefore remain valid throughout the lifetime of the application, i.e. they can be materialized as often as required and the time between individual materialization is of no importance.
When you request a pool client flow with
Http().cachedHostConnectionPool(...) Akka HTTP will immediately start
the pool, even before the first client flow materialization. However, this running pool will not actually open the
first connection to the target endpoint until the first request has arrived.
Configuring a Host Connection Pool
Apart from the connection-level config settings and socket options there are a number of settings that allow you to
influence the behavior of the connection pool logic itself.
Check out the
akka.http.host-connection-pool section of the Akka HTTP Configuration for
more information about which settings are available and what they mean.
Note that, if you request pools with different configurations for the same target host you will get independent pools.
This means that, in total, your application might open more concurrent HTTP connections to the target endpoint than any
of the individual pool's
max-connections settings allow!
There is one setting that likely deserves a bit deeper explanation:
This setting limits the maximum number of requests that can be in-flight at any time for a single connection pool.
If an application calls
Http().cachedHostConnectionPool(...) 3 times (with the same endpoint and settings) it will get
3 different client flow instances for the same pool. If each of these client flows is then materialized
(concurrently) the application will have 12 concurrently running client flow materializations.
All of these share the resources of the single pool.
This means that, if the pool's
pipelining-limit is left at
1 (effecitvely disabeling pipelining), no more than 12 requests can be open at any time.
8 and 12 concurrent client flow materializations the theoretical open requests
max-open-requests config setting allows for applying a hard limit which serves mainly as a protection against
erroneous connection pool use, e.g. because the application is materializing too many client flows that all compete for
the same pooled connections.
Using a Host Connection Pool
The "pool client flow" returned by
Http().cachedHostConnectionPool(...) has the following type:
Flow[(HttpRequest, T), (Try[HttpResponse], T), HostConnectionPool]
This means it consumes tuples of type
(HttpRequest, T) and produces tuples of type
which might appear more complicated than necessary on first sight.
The reason why the pool API includes objects of custom type
T on both ends lies in the fact that the underlying
transport usually comprises more than a single connection and as such the pool client flow often generates responses in
an order that doesn't directly match the consumed requests.
We could have built the pool logic in a way that reorders responses according to their requests before dispatching them
to the application, but this would have meant that a single slow response could block the delivery of potentially many
responses that would otherwise be ready for consumption by the application.
In order to prevent unnecessary head-of-line blocking the pool client-flow is allowed to dispatch responses as soon as
they arrive, independently of the request order. Of course this means that there needs to be another way to associate a
response with its respective request. The way that this is done is by allowing the application to pass along a custom
"context" object with the request, which is then passed back to the application with the respective response.
This context object of type
T is completely opaque to Akka HTTP, i.e. you can pick whatever works best for your
particular application scenario.
A consequence of using a pool is that long-running requests block a connection while running and may starve other requests. Make sure not to use a connection pool for long-running requests like long-polling GET requests. Use the Connection-Level Client-Side API instead.
Connection Allocation Logic
This is how Akka HTTP allocates incoming requests to the available connection "slots":
- If there is a connection alive and currently idle then schedule the request across this connection.
- If no connection is idle and there is still an unconnected slot then establish a new connection.
- If all connections are already established and "loaded" with other requests then pick the connection with the least
open requests (< the configured
pipelining-limit) that only has requests with idempotent methods scheduled to it, if there is one.
- Otherwise apply back-pressure to the request source, i.e. stop accepting new requests.
For more information about scheduling more than one request at a time across a single connection see this wikipedia entry on HTTP pipelining.
Retrying a Request
max-retries pool config setting is greater than zero the pool retries idempotent requests for which
a response could not be successfully retrieved. Idempotent requests are those whose HTTP method is defined to be
idempotent by the HTTP spec, which are all the ones currently modelled by Akka HTTP except for the
When a response could not be received for a certain request there are essentially three possible error scenarios:
- The request got lost on the way to the server.
- The server experiences a problem while processing the request.
- The response from the server got lost on the way back.
Since the host connector cannot know which one of these possible reasons caused the problem and therefore
POST requests could have already triggered a non-idempotent action on the server these requests cannot be retried.
In these cases, as well as when all retries have not yielded a proper response, the pool produces a failed
scala.util.Failure) together with the custom request context.
Completing a pool client flow will simply detach the flow from the pool. The connection pool itself will continue to run
as it may be serving other client flows concurrently or in the future. Only after the configured
the pool has expired will Akka HTTP automatically terminate the pool and free all its resources.
If a new client flow is requested with
Http().cachedHostConnectionPool(...) or if an already existing client flow is
re-materialized the respective pool is automatically and transparently restarted.
In addition to the automatic shutdown via the configured idle timeouts it's also possible to trigger the immediate
shutdown of a specific pool by calling
shutdown() on the
HostConnectionPool instance that the pool client
flow materializes into. This
shutdown() call produces a
Future[Unit] which is fulfilled when the pool
termination has been completed.
It's also possible to trigger the immediate termination of all connection pools in the
ActorSystem at the same
time by calling
Http().shutdownAllConnectionPools(). This call too produces a
Future[Unit] which is fulfilled when
all pools have terminated.
When encoutering unexpected
akka.stream.AbruptTerminationException exceptions during
please make sure that active connections are shut down before shutting down the entire system, this can be done by
Http().shutdownAllConnectionPools() method, and only once its Future completes, shutting down the actor system.
import akka.http.scaladsl.Http import akka.http.scaladsl.model._ import akka.stream.ActorMaterializer import akka.stream.scaladsl._ import scala.concurrent.Future import scala.util.Try implicit val system = ActorSystem() implicit val materializer = ActorMaterializer() // construct a pool client flow with context type `Int` val poolClientFlow = Http().cachedHostConnectionPool[Int]("akka.io") val responseFuture: Future[(Try[HttpResponse], Int)] = Source.single(HttpRequest(uri = "/") -> 42) .via(poolClientFlow) .runWith(Sink.head)