Consuming HTTP-based Services (Client-Side)

Một phần của tài liệu Akka scala (Trang 730 - 744)

All client-side functionality of Akka HTTP, for consuming HTTP-based services offered by other endpoints, is currently provided by theakka-http-coremodule.

It is recommended to first read theImplications of the streaming nature of Request/Response Entitiessection, as it explains the underlying full-stack streaming concepts, which may be unexpected when coming from a background with non-“streaming first” HTTP Clients.

Depending on your application’s specific needs you can choose from three different API levels:

Connection-Level Client-Side API for full-control over when HTTP connections are opened/closed and how re- quests are scheduled across them

Host-Level Client-Side API for letting Akka HTTP manage a connection-pool toone specifichost/port endpoint Request-Level Client-Side API for letting Akka HTTP perform all connection management

You can interact with different API levels at the same time and, independently of which API level you choose, Akka HTTP will happily handle many thousand concurrent connections to a single or many different hosts.

8.7.1 Connection-Level Client-Side API

The connection-level API is the lowest-level client-side API Akka HTTP provides. It gives you full control over when HTTP connections are opened and closed and how requests are to be send across which connection. As such it offers the highest flexibility at the cost of providing the least convenience.

Note: It is recommended to first read theImplications of the streaming nature of Request/Response Entities section, as it explains the underlying full-stack streaming concepts, which may be unexpected when coming from a background with non-“streaming first” HTTP Clients.

Opening HTTP Connections

With the connection-level API you open a new HTTP connection to a target endpoint by materializing aFlow returned by theHttp().outgoingConnection(...)method. Here is an example:

import akka.actor.ActorSystem 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.{ Failure, Success } object WebClient {

8.7. Consuming HTTP-based Services (Client-Side) 726

def main(args: Array[String]): Unit = { implicit val system = ActorSystem()

implicit val materializer = ActorMaterializer() implicit val executionContext = system.dispatcher

val connectionFlow: Flow[HttpRequest, HttpResponse, Future[Http.

˓→OutgoingConnection]] =

Http().outgoingConnection("akka.io") val responseFuture: Future[HttpResponse] =

Source.single(HttpRequest(uri = "/")) .via(connectionFlow)

.runWith(Sink.head) responseFuture.andThen {

case Success(_) => println("request succeded") case Failure(_) => println("request failed") }.andThen {

case _ => system.terminate() }

} }

Apart from the host name and port theHttp().outgoingConnection(...) method also allows you to specify socket options and a number of configuration settings for the connection.

Note that no connection is attempted until the returned flow is actually materialized! If the flow is materialized several times then several independent connections will be opened (one per materialization). If the connection at- tempt fails, for whatever reason, the materialized flow will be immediately terminated with a respective exception.

Request-Response Cycle

Once the connection flow has been materialized it is ready to consumeHttpRequestinstances from the source it is attached to. Each request is sent across the connection and incoming responses dispatched to the downstream pipeline. Of course and as always, back-pressure is adequately maintained across all parts of the connection. This means that, if the downstream pipeline consuming the HTTP responses is slow, the request source will eventually be slowed down in sending requests.

Any errors occurring on the underlying connection are surfaced as exceptions terminating the response stream (and canceling the request source).

Note that, if the source produces subsequent requests before the prior responses have arrived, these requests will bepipelinedacross the connection, which is something that is not supported by all HTTP servers. Also, if the server closes the connection before responses to all requests have been received this will result in the response stream being terminated with a truncation error.

Closing Connections

Akka HTTP actively closes an established connection upon reception of a response containingConnection:

closeheader. The connection can also be closed by the server.

An application can actively trigger the closing of the connection by completing the request stream. In this case the underlying TCP connection will be closed when the last pending response has been received.

The connection will also be closed if the response entity is cancelled (e.g. by attaching it toSink.cancelled) or consumed only partially (e.g. by usingtakecombinator). In order to prevent this behaviour the entity should be explicitly drained by attaching it toSink.ignore.

8.7. Consuming HTTP-based Services (Client-Side) 727

Timeouts

Currently Akka HTTP doesn’t implement client-side request timeout checking itself as this functionality can be regarded as a more general purpose streaming infrastructure feature.

It should be noted that Akka Streams provide various timeout functionality so any API that uses streams can benefit from the stream stages such asidleTimeout,backpressureTimeout,completionTimeout, initialTimeoutandthrottle. To learn more about these refer to their documentation in Akka Streams (and Scala Doc).

For more details about timeout support in Akka HTTP in general refer toAkka HTTP Timeouts.

Stand-Alone HTTP Layer Usage

Due to its Reactive-Streams-based nature the Akka HTTP layer is fully detachable from the underlying TCP interface. While in most applications this “feature” will not be crucial it can be useful in certain cases to be able to “run” the HTTP layer (and, potentially, higher-layers) against data that do not come from the network but rather some other source. Potential scenarios where this might be useful include tests, debugging or low-level event-sourcing (e.g by replaying network traffic).

On the client-side the stand-alone HTTP layer forms aBidiStagethat is defined like this:

/**

* The type of the client-side HTTP layer as a stand-alone BidiFlow

* that can be put atop the TCP layer to form an HTTP client.

*

* {{{

* +---+

* HttpRequest ~>| |~> SslTlsOutbound

* | bidi |

* HttpResponse <~| |<~ SslTlsInbound

* +---+

* }}}

*/

type ClientLayer = BidiFlow[HttpRequest, SslTlsOutbound, SslTlsInbound,

˓→HttpResponse, NotUsed]

You create an instance of Http.ClientLayer by calling one of the two overloads of the Http().clientLayermethod, which also allows for varying degrees of configuration.

8.7.2 Host-Level Client-Side API

As opposed to the Connection-Level Client-Side APIthe host-level API relieves you from manually managing individual HTTP connections. It autonomously manages a configurable pool of connections toone particular target endpoint(i.e. host/port combination).

Note: It is recommended to first read theImplications of the streaming nature of Request/Response Entities section, as it explains the underlying full-stack streaming concepts, which may be unexpected when coming from a background with non-“streaming first” HTTP Clients.

Requesting a Host Connection Pool

The best way to get a hold of a connection pool to a given target endpoint is the Http().cachedHostConnectionPool(...) 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 everyActorSystem, target endpoint and pool configuration there will never be more than a single pool live at any time.

8.7. Consuming HTTP-based Services (Client-Side) 728

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 withHttp().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-poolsection of the Akka HTTPConfigurationfor 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 getindependent pools. This means that, in total, your application might open more concurrent HTTP connections to the target endpoint than any of the individual pool’smax-connectionssettings allow!

There is one setting that likely deserves a bit deeper explanation: max-open-requests. This setting limits the maximum number of requests that can be in-flight at any time for a single connection pool. If an application callsHttp().cachedHostConnectionPool(...) 3 times (with the same endpoint and settings) it will get back3different client flow instances for the same pool. If each of these client flows is then materialized4 times (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 at1 (effecitvely disabeling pipelining), no more than 12 requests can be open at any time. With a pipelining-limit of8 and 12 concurrent client flow materializations the theoretical open requests maximum is96.

Themax-open-requestsconfig 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 byHttp().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 (Try[HttpResponse],T) 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 typeTis completely opaque to Akka HTTP, i.e. you can pick whatever works best for your particular application scenario.

Note: 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 theConnection-Level Client-Side APIinstead.

8.7. Consuming HTTP-based Services (Client-Side) 729

Connection Allocation Logic

This is how Akka HTTP allocates incoming requests to the available connection “slots”:

1. If there is a connection alive and currently idle then schedule the request across this connection.

2. If no connection is idle and there is still an unconnected slot then establish a new connection.

3. If all connections are already established and “loaded” with other requests then pick the connection with the least open requests (< the configuredpipelining-limit) that only has requests with idempotent methods scheduled to it, if there is one.

4. 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

If themax-retriespool 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 thePOST, PATCHandCONNECTmethods.

When a response could not be received for a certain request there are essentially three possible error scenarios:

1. The request got lost on the way to the server.

2. The server experiences a problem while processing the request.

3. 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 PATCHandPOSTrequests 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 failedTry(i.e.

ascala.util.Failure) together with the custom request context.

Pool Shutdown

Completing a pool client flow will simply detach the flow from the pool. The connection pool itself will con- tinue to run as it may be serving other client flows concurrently or in the future. Only after the configured idle-timeoutfor the pool has expired will Akka HTTP automatically terminate the pool and free all its re- sources.

If a new client flow is requested withHttp().cachedHostConnectionPool(...)or if an already exist- ing 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 HostConnectionPoolinstance that the pool client flow materializes into. Thisshutdown()call produces a Future[Unit]which is fulfilled when the pool termination has been completed.

It’s also possible to trigger the immediate termination ofallconnection pools in theActorSystemat the same time by callingHttp().shutdownAllConnectionPools(). This call too produces aFuture[Unit]

which is fulfilled when all pools have terminated.

Note: When encoutering unexpectedakka.stream.AbruptTerminationExceptionexceptions during ActorSystemshutdown please make sure that active connections are shut down before shutting down the entire system, this can be done by calling theHttp().shutdownAllConnectionPools()method, and only once its Future completes, shutting down the actor system.

8.7. Consuming HTTP-based Services (Client-Side) 730

Example

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)

8.7.3 Request-Level Client-Side API

The request-level API is the most convenient way of using Akka HTTP’s client-side functionality. It internally builds upon theHost-Level Client-Side APIto provide you with a simple and easy-to-use way of retrieving HTTP responses from remote servers. Depending on your preference you can pick the flow-based or the future-based variant.

Note: It is recommended to first read theImplications of the streaming nature of Request/Response Entities section, as it explains the underlying full-stack streaming concepts, which may be unexpected when coming from a background with non-“streaming first” HTTP Clients.

Note: The request-level API is implemented on top of a connection pool that is shared inside the ActorSystem.

A consequence of using a pool is that long-running requests block a connection while running and starve other requests. Make sure not to use the request-level API for long-running requests like long-polling GET requests.

Use theConnection-Level Client-Side APIinstead.

Flow-Based Variant

The flow-based variant of the request-level client-side API is presented by the Http().superPool(...) method. It creates a new “super connection pool flow”, which routes incoming requests to a (cached) host con- nection pool depending on their respective effective URIs.

TheFlowreturned byHttp().superPool(...) is very similar to the one from theHost-Level Client-Side API, so theUsing a Host Connection Poolsection also applies here.

However, there is one notable difference between a “host connection pool client flow” for the host-level API and a

“super-pool flow”: Since in the former case the flow has an implicit target host context the requests it takes don’t need to have absolute URIs or a validHostheader. The host connection pool will automatically add aHost header if required.

For a super-pool flow this is not the case. All requests to a super-pool must either have an absolute URI or a valid Hostheader, because otherwise it’d be impossible to find out which target endpoint to direct the request to.

8.7. Consuming HTTP-based Services (Client-Side) 731

Future-Based Variant

Sometimes your HTTP client needs are very basic. You simply need the HTTP response for a certain request and don’t want to bother with setting up a full-blown streaming infrastructure.

For these cases Akka HTTP offers the Http().singleRequest(...) method, which simply turns an HttpRequest instance into Future[HttpResponse]. Internally the request is dispatched across the (cached) host connection pool for the request’s effective URI.

Just like in the case of the super-pool flow described above the request must have either an absolute URI or a valid Hostheader, otherwise the returned future will be completed with an error.

Using the Future-Based API in Actors

When using theFuturebased API from inside anActor, all the usual caveats apply to how one should deal with the futures completion. For example you should not access the Actors state from within the Future’s callbacks (such asmap,onComplete, ...) and instead you should use thepipeTopattern to pipe the result back to the Actor as a message.

import akka.actor.Actor

import akka.http.scaladsl.Http import akka.http.scaladsl.model._

import akka.stream.ActorMaterializer

import akka.stream.ActorMaterializerSettings 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, _) =>

log.info("Got response, body: " + entity.dataBytes.runFold(ByteString(""))(_

˓→++ _))

case resp @ HttpResponse(code, _, _, _) =>

log.info("Request failed, response code: " + code) resp.discardEntityBytes()

} }

Example

import akka.http.scaladsl.Http import akka.http.scaladsl.model._

import akka.stream.ActorMaterializer import scala.concurrent.Future

import scala.util.{ Failure, Success }

8.7. Consuming HTTP-based Services (Client-Side) 732

Một phần của tài liệu Akka scala (Trang 730 - 744)

Tải bản đầy đủ (PDF)

(857 trang)