Description
Currently, Task
has an eventLoop
stored property, and its channel
must be on this same event loop.
In practice, eventLoop
can be used by the http client delegate in the following ways:
- Return an immediately succeeded future in the appropriate methods when they don’t want to apply back pressure.
- Schedule backpressure related work on this event loop and return the backpressure control future.
- In case backpressure work is done on another event loop (which might be more often the case than Update readme with simple tutorial #2), hop the resulting backpressure future to
eventLoop
.
In these three cases, the goal to guarantee to the http client that it can assume all futures it gets from its delegate are already on the right event loop.
Potential shortcomings
Errors on user side
When applying back pressure, users getting futures from different event loops might forget to hop them back to the right event loop. On the other hand, if the client stopped relying on the assumption that futures returned from the delegate are already on the right event loop, and instead always took care of hopping them, this source of bugs would disappear entirely for a minimal performance cost (hopTo isn’t too heavy, and in many cases the user will need to use it either way, so why force them to do it when we could do this on the library side?).
Hard to compose with future evolutions
The eventLoop
property might not compose very well with future evolutions of the client such as pooling (tldr: the associated event loop is hard to determine synchronously like Task needs, and Task might change of event loop during its lifetime):
- It complicates the connection generation logic on the pool side. For instance a connection pool would likely contain a method such as
getConnection(for request: HTTPClient.Request -> EventLoopFuture<Connection>
, this is logical for it to return a future because a connection might not be instantly available, but it couldn’t be used as such with the current AsyncClient public API:execute()
synchronously returnsTask
, butTask
needs aneventLoop
on initialization, which couldn’t be instantaneously determined due togetConnection
being an async method. There would of be workarounds for this, but they would complicate the connection pool logic (maybe not a really good sign when two isolated components start to interact this way) and may also force creating new connections (instead of using released ones) just to satisfy the event loop identity constraint. - Pooling will also need a retry logic for when a connection dies too soon, this means associating the Task with a new channel that will probably not be on the same event loop as before except if we always made a new connection for retries, but it would be a ressource waste.
Proposed solution
Remove eventLoop
property entirely, always hop futures provided by the delegate, additionally, update HTTPClientResponseDelegate
backpressure controlling methods to make them return EventLoopFuture<Void>?
instead of a non-optional EventLoopFuture<Void>
.
This change would enable the following:
- Reduces the risk of threading issue in delegate implementations
- Makes
Task
less dependent on a specific event loop, making it easier to evolve in the future. - Delegate implementations that want to schedule work on the same event loop as channel can still make use of
task.channel.eventLoop
knowing it might evolve during theTask
lifetime. - Delegate implementations that don't wish to exerce backpressure can express it more clearly/easily by returning
nil
instead of having to referenceeventLoop
just to return an immediately completed void future.
What do you think about this? Am I missing some important part?
Thank you