Background
Service is a push based API. Requests are pushed into the service. This makes it vulnerable to situations where the request producer generates requests faster than the service is able to process them.
To mitigate this, Service provides a back pressure strategy based on the Service::poll_ready. Before pushing a new request into the service, the producer must call Service::poll_ready. The service is then able to inform the producer if it is ready to handle the new request. If it is not ready, the service will notify the producer when it becomes ready using the task notification system.
Services and concurrency
The Service::call function returns immediately with a future of the response. The producer may call the service repeatedly with new requests before previously returned futures complete. This allows a single service value to concurrently process requests.
This behavior complicates being able to sequentially call services. Consider the and_then combinator:
let combined = service1.and_then(service2);
combined.ready()
.and_then(|combined| combined.call(my_request));
This will result in my_request being pushed into service1, and when the returned future completes, the response is pushed into service2. Because the response from service1 completes asynchronously, the combined response future must contain a handle to service2. Because the Service trait requires &mut self in order to use, service2 cannot be stored in an Arc and cloned into the shared response future.
The strategy to handle this is to require service2 to implement Clone and let each service implementation manage how it will handle dealing with concurrency. Concurrency can be added to any service by adding a layer of message passing. A task is spawned to drive the service itself and a channel is used to buffer queued requests. The channel sender implements Service. This pattern is provided by buffer.
Problem
The question at hand is how to combine the back pressure API (poll_ready) with the pattern of cloning service handles to handle concurrency.
The first option is to use the same strategy as channels for back pressure. In short, each Sender handle has a dedicated buffer of 1 slot (see here for a detailed discussion). Given that it is permitted to clone Service handles once per request, applying this behavior to Service would effectively result in unbounded buffering.
Proposal
Instead, Service::poll_ready should use a reservation strategy. Calling Service::poll_ready results in reserving the capacity for the producer to send one request. Once poll_ready returns Ready, the next invocation of call will not result in an out of capacity error.
This strategy also means that it is possible for a service's capacity to be depleted without any requests being in-flight yet. Consider buffer with a capacity of 1. A service calls poll_ready before generating the request. In order guarantee that that capacity is available when the request has been produced, the service must reserve a slot. The service then has no remaining capacity but the request has yet to be produced.
The and_then combinator
In the case of the and_then combinator, poll_ready is forwarded to both services. For the combined service to be ready, capacity must be reserved in both the first and the second service. This means that, if the response future for the first service takes significant time to complete, the second service could be starved. This can be mitigated somewhat by adding additional buffering to the second service.
cc @olix0r @seanmonstar
Background
Serviceis a push based API. Requests are pushed into the service. This makes it vulnerable to situations where the request producer generates requests faster than the service is able to process them.To mitigate this,
Serviceprovides a back pressure strategy based on theService::poll_ready. Before pushing a new request into the service, the producer must callService::poll_ready. The service is then able to inform the producer if it is ready to handle the new request. If it is not ready, the service will notify the producer when it becomes ready using the task notification system.Services and concurrency
The
Service::callfunction returns immediately with a future of the response. The producer may call the service repeatedly with new requests before previously returned futures complete. This allows a single service value to concurrently process requests.This behavior complicates being able to sequentially call services. Consider the
and_thencombinator:This will result in
my_requestbeing pushed intoservice1, and when the returned future completes, the response is pushed intoservice2. Because the response fromservice1completes asynchronously, the combined response future must contain a handle toservice2. Because theServicetrait requires&mut selfin order to use,service2cannot be stored in anArcand cloned into the shared response future.The strategy to handle this is to require
service2to implementCloneand let each service implementation manage how it will handle dealing with concurrency. Concurrency can be added to any service by adding a layer of message passing. A task is spawned to drive the service itself and a channel is used to buffer queued requests. The channel sender implementsService. This pattern is provided bybuffer.Problem
The question at hand is how to combine the back pressure API (
poll_ready) with the pattern of cloning service handles to handle concurrency.The first option is to use the same strategy as channels for back pressure. In short, each
Senderhandle has a dedicated buffer of 1 slot (see here for a detailed discussion). Given that it is permitted to cloneServicehandles once per request, applying this behavior toServicewould effectively result in unbounded buffering.Proposal
Instead,
Service::poll_readyshould use a reservation strategy. CallingService::poll_readyresults in reserving the capacity for the producer to send one request. Oncepoll_readyreturnsReady, the next invocation ofcallwill not result in an out of capacity error.This strategy also means that it is possible for a service's capacity to be depleted without any requests being in-flight yet. Consider
bufferwith a capacity of 1. A service callspoll_readybefore generating the request. In order guarantee that that capacity is available when the request has been produced, the service must reserve a slot. The service then has no remaining capacity but the request has yet to be produced.The
and_thencombinatorIn the case of the
and_thencombinator,poll_readyis forwarded to both services. For the combined service to be ready, capacity must be reserved in both the first and the second service. This means that, if the response future for the first service takes significant time to complete, the second service could be starved. This can be mitigated somewhat by adding additional buffering to the second service.cc @olix0r @seanmonstar