akka / akka-http

The Streaming-first HTTP server/module of Akka
https://doc.akka.io/libraries/akka-http/current/
Other
1.34k stars 594 forks source link

Remove `max-open-requests` constraint on Flow-Based APIs #2120

Open patrick-premont opened 6 years ago

patrick-premont commented 6 years ago

I think it's natural to expect Flow-based APIs to backpressure, yet the max-open-requestsetting violates that expectation, introducing run-time errors instead of back-pressure.

This answer appears to agree with that expectation: https://discuss.lightbend.com/t/difference-between-http-singlerequest-and-http-superpool/713

Despite some efforts of the documentation, it is not clear how to set max-open-requests to guarantee those errors will never occur. Apparently, more materializations that share the same pool necessitates a higher value for the setting? If so that suggests more requests would be "open" than what the shared pooled is supported to allow.

It appears that this setting is there to facilitate the implementation, as a buffer of that size is allocated.

Perhaps removing this constraint would require using a MergeHub?

There are issues that relate to max-open-requests in Flow-Based APIs that attest to the difficulties it introduces: #843 #125

jrudolph commented 5 years ago

We never answered here (but maybe discussed it somewhere else?). In any case, it seems like a good idea.

jrudolph commented 5 years ago

I'll try to describe why it is the way it is currently and what we could do about it in 10.2.0.

The first thing to understand is the difference in how Http.singleRequest(), Http().cachedHostConnectionPoolHttps and Http().superPool are implemented. Requests coming in through any of those entry point APIs may end up in the same shared pool. singleRequest is a push-based Future API while the others are streaming APIs.

Currently, a shared pool to a host is implemented with the functionality of Http().singleRequest in mind. Since a while, this is abstracted as the PoolInterface which offers a push-based request method to run a single request. PoolInterfaces are then registered in the PoolMasterActor which dispatches a request based on the target host and spawns pools to a host as needed.

The pool itself (NewHostConnectionPool) and the interface in front of it are implemented as a streaming API. To support the push-based PoolInterface API, a buffer is needed to store requests if they come in a burst. The size of that buffer is goverened by the max-open-requests setting (which is and has been a misnomer since the beginning).

The streaming based APIs cachedHostConnectionPoolHttps and superPool are just implemented on top of that push-based API, using mapAsyncUnordered to offer requests to the pool using the pool interface. That means, however, that there's no API that would offer end-to-end backpressure. Instead, parallelism is limited just by the value given to mapAsyncUnordered which is hardcoded to be settings.pipeliningLimit * settings.maxConnections. This makes somewhat sense because it tries to queue up no more requests in front of a pool as the pool may run concurrently. As @patrick-premont noticed, it breaks down as soon as you attach multiple such streams to a shared pool that all share a fixed number of max-open-requests that might be smaller than settings.pipeliningLimit * settings.maxConnections * number of streams.

Incidentally, the superPool API doesn't seem to be useful because the superPool flow has to group by target hosts, so there can be head-of-line blocking when the stream mixes requests to fast and slow hosts (there's nothing we can do about that, it's inherent in the streaming superPool API).

That still leaves cachedHostConnectionPoolHttps and newHostConnectionPool for both of which an end-to-end backpressured API would make sense. Otherwise, with the current API, what should you set the value of max-open-requests to? The safe choice would be to set it to settings.pipeliningLimit * settings.maxConnections * number of streams but that means that 1) you need a static limit of streams and 2) you will usually have an oversized buffer in front of the pool adding to the end-to-end latency you will measure when using that pool.

So much for the current situation.

jrudolph commented 5 years ago

Now to what we could do about it in the future.

The pool itself is already implemented in a streaming way, so it should be easy enough to offer that as an API. As suggested we could put use a MergeHub / BroadcastHub to offer that streaming API to multiple consumers. However, with this sharing come a few new complications:

So, in summary, we'll need:

jrudolph commented 4 years ago

Pushed back for now.