Skip to content

Commit a1ca482

Browse files
committed
Splitting up the big protocol
1 parent 461ab39 commit a1ca482

File tree

4 files changed

+278
-163
lines changed

4 files changed

+278
-163
lines changed

Sources/AsyncHTTPClient/ConnectionPool/HTTPClientTask.swift

Lines changed: 0 additions & 136 deletions
This file was deleted.
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Logging
16+
import NIO
17+
import NIOHTTP1
18+
19+
/// # Protocol Overview
20+
///
21+
/// To support different public request APIs we abstract the actual request implementations behind
22+
/// protocols. During the lifetime of a request, a request must conform to different protocols
23+
/// depending on which state it is in.
24+
///
25+
/// Generally there are two main states in a request's lifetime:
26+
///
27+
/// 1. **The request is scheduled to be run.**
28+
/// In this state the HTTP client tries to acquire a connection for the request, and the request
29+
/// may need to wait for a connection)
30+
/// 2. **The request is executing.**
31+
/// In this state the request was written to a NIO channel. A NIO channel handler (abstracted
32+
/// by the `HTTPRequestExecutor` protocol) writes the request's bytes onto the wire and
33+
/// dispatches the http response bytes back to the response.
34+
///
35+
///
36+
/// ## Request is scheduled
37+
///
38+
/// When the `HTTPClient` shall send an HTTP request, it will use its `HTTPConnectionPool.Manager` to
39+
/// determine the `HTTPConnectionPool` to run the request on. After a `HTTPConnectionPool` has been
40+
/// found for the request, the request will be executed on this connection pool. Since the HTTP
41+
/// request implements the `HTTPScheduledRequest` protocol, the HTTP connection pool can communicate
42+
/// with the request. The `HTTPConnectionPool` implements the `HTTPRequestScheduler` protocol.
43+
///
44+
/// 1. The `HTTPConnectionPool` tries to find an idle connection for the request based on its
45+
/// `eventLoopPreference`.
46+
///
47+
/// 2. If an idle connection is available to the request, the request will be passed to the
48+
/// connection right away. In this case the `HTTPConnectionPool` will only use the
49+
/// `HTTPScheduledRequest`'s `eventLoopPreference` property. No other methods will be called.
50+
///
51+
/// 3. If no idle connection is available to the request, the request will be queued for execution:
52+
/// - The `HTTPConnectionPool` will inform the request that it is queued for execution by
53+
/// calling: `requestWasQueued(_: HTTPRequestScheduler)`. The request must store a reference
54+
/// to the `HTTPRequestScheduler`. The request must call `cancelRequest(self)` on the
55+
/// scheduler, if the request was cancelled, while waiting for execution.
56+
/// - The `HTTPConnectionPool` will create a connection deadline based on the
57+
/// `HTTPScheduledRequest`'s `connectionDeadline` property. If a connection to execute the
58+
/// request on, was not found before this deadline the request will be failed.
59+
/// - The HTTPConnectionPool will call `fail(_: Error)` on the `HTTPScheduledRequest` to
60+
/// inform the request about having overrun the `connectionDeadline`.
61+
///
62+
///
63+
/// ## Request is executing
64+
///
65+
/// After the `HTTPConnectionPool` has identified a connection for the request to be run on, it will
66+
/// execute the request on this connection. (Implementation detail: This happens by writing the
67+
/// `HTTPExecutingRequest` to a `NIO.Channel`. We expect the last handler in the `ChannelPipeline`
68+
/// to have an `OutboundIn` type of `HTTPExecutingRequest`. Further we expect that the handler
69+
/// also conforms to the protocol `HTTPRequestExecutor` to allow communication of the request with
70+
/// the executor/`ChannelHandler`).
71+
///
72+
/// The request execution will work as follows:
73+
///
74+
/// 1. The request executor will call `willExecuteRequest(_: HTTPRequestExecutor) -> Bool` on the
75+
/// request. The request is expected to return `true`, if it wants to be executed and false if
76+
///     it was cancelled or failed, before this checkpoint. If the request returns `true` it is
77+
/// expected to keep a reference to the `HTTPRequestExecutor` that was passed to the request
78+
/// for further communication. If the request returned false, the executor shall inform the
79+
/// scheduling mechanism that it is ready to execute the a new request.
80+
/// 2. The request sending is started by the executor accessing the `HTTPExecutingRequest`'s
81+
/// property `requestHead: HTTPRequestHead`. Based on the `requestHead` the executor can
82+
/// determine if the request has a body (Is a "content-length" or "transfer-encoding"
83+
/// header present?).
84+
/// 3. The executor will write the request's header into the Channel. If no body is present, the
85+
/// executor will also write a request end into the Channel. After this the executor will call
86+
/// `requestHeadSent(_: HTTPRequestHead)`
87+
/// 4. If the request has a body the request executor will, ask the request for body data, by
88+
/// calling `startRequestBodyStream()`. The request is expected to call
89+
/// `writeRequestBodyPart(_: IOData, task: HTTPExecutingRequest)` on the executor with body
90+
/// data.
91+
/// - The executor can signal backpressure to the request by calling
92+
/// `pauseRequestBodyStream()`. In this case the request is expected to stop calling
93+
/// `writeRequestBodyPart(_: IOData, task: HTTPExecutingRequest)`. However because of race
94+
/// conditions the executor is prepared to process more data, even though it asked the
95+
/// request to pause.
96+
/// - Once the executor is able to send more data, it will notify the request by calling
97+
/// `resumeRequestBodyStream()` on the request.
98+
/// - The request shall call `finishRequestBodyStream()` on the executor to signal that the
99+
/// request body was sent.
100+
/// 5. Once the executor receives a http response from the Channel, it will forward the http
101+
/// response head to the `HTTPExecutingRequest` by calling `receiveResponseHead` on it.
102+
/// - The executor will forward all the response body parts it receives in a single read to
103+
/// the `HTTPExecutingRequest` without any buffering by calling
104+
/// `receiveResponseBodyPart(_ buffer: ByteBuffer)` right away. It is the task's job to
105+
/// buffer the responses for user consumption.
106+
/// - Once the executor has finished a read, it will not schedule another read, until the
107+
/// request calls `demandResponseBodyStream(task: HTTPExecutingRequest)` on the executor.
108+
/// - Once the executor has received the response's end, it will forward this message by
109+
/// calling `receiveResponseEnd()` on the `HTTPExecutingRequest`.
110+
/// 6. If a channel error occurs during the execution of the request, or if the channel becomes
111+
/// inactive the executor will notify the request by calling `fail(_ error: Error)` on it.
112+
/// 7. If the request is cancelled, while it is executing on the executor, it must call
113+
/// `cancelRequest(task: HTTPExecutingRequest)` on the executor.
114+
///
115+
///
116+
/// ## Further notes
117+
///
118+
/// - These protocols makes no guarantees about thread safety at all. It is implementations job to
119+
/// ensure thread safety.
120+
/// - However all calls to the `HTTPRequestScheduler` and `HTTPRequestExecutor` require that the
121+
/// invoking request is passed along. This helps the scheduler and executor in race conditions.
122+
/// Example:
123+
/// - The executor may have received an error in thread A that it passes along to the request.
124+
/// After having passed on the error, the executor considers the request done and releases
125+
/// the request's reference.
126+
/// - The request may issue a call to `writeRequestBodyPart(_: IOData, task: HTTPExecutingRequest)`
127+
/// on thread B in the same moment the request error above occurred. For this reason it may
128+
/// happen that the executor receives, the invocation of `writeRequestBodyPart` after it has
129+
/// failed the request.
130+
/// Passing along the requests reference helps the executor and scheduler verify its internal
131+
/// state.
132+
133+
/// A handle to the request queuer.
134+
///
135+
/// Use this handle to cancel the request, while it is waiting for a free connection, to execute the request.
136+
/// This protocol is only intended to be implemented by the `HTTPConnectionPool`.
137+
protocol HTTPRequestScheduler {
138+
/// Informs the task queuer that a request has been cancelled.
139+
func cancelRequest(_: HTTPScheduledRequest)
140+
}
141+
142+
/// An abstraction over a request that we want to send. A request may need to communicate with its request
143+
/// queuer and executor. The client's methods will be called synchronously on an `EventLoop` by the
144+
/// executor. For this reason it is very important that the implementation of these functions never blocks.
145+
protocol HTTPScheduledRequest: AnyObject {
146+
/// The task's logger
147+
var logger: Logger { get }
148+
149+
/// A connection to run this task on needs to be found before this deadline!
150+
var connectionDeadline: NIODeadline { get }
151+
152+
/// The task's eventLoopPreference
153+
var eventLoopPreference: HTTPClient.EventLoopPreference { get }
154+
155+
/// Informs the task, that it was queued for execution
156+
///
157+
/// This happens if all available connections are currently in use
158+
func requestWasQueued(_: HTTPRequestScheduler)
159+
160+
/// Fails the queued request, with an error.
161+
func fail(_ error: Error)
162+
}
163+
164+
/// A handle to the request executor.
165+
///
166+
/// This protocol is implemented by the `HTTP1ClientChannelHandler`.
167+
protocol HTTPRequestExecutor {
168+
/// Writes a body part into the channel pipeline
169+
///
170+
/// This method may be **called on any thread**. The executor needs to ensure thread safety.
171+
func writeRequestBodyPart(_: IOData, request: HTTPExecutingRequest)
172+
173+
/// Signals that the request body stream has finished
174+
///
175+
/// This method may be **called on any thread**. The executor needs to ensure thread safety.
176+
func finishRequestBodyStream(_ task: HTTPExecutingRequest)
177+
178+
/// Signals that more bytes from response body stream can be consumed.
179+
///
180+
/// The request executor will call `receiveResponseBodyPart(_ buffer: ByteBuffer)` with more data after
181+
/// this call.
182+
///
183+
/// This method may be **called on any thread**. The executor needs to ensure thread safety.
184+
func demandResponseBodyStream(_ task: HTTPExecutingRequest)
185+
186+
/// Signals that the request has been cancelled.
187+
///
188+
/// This method may be **called on any thread**. The executor needs to ensure thread safety.
189+
func cancelRequest(_ task: HTTPExecutingRequest)
190+
}
191+
192+
protocol HTTPExecutingRequest: AnyObject {
193+
/// The request's head.
194+
///
195+
/// Based on the content of the request head the task executor will call `startRequestBodyStream`
196+
/// after `requestHeadSent` was called.
197+
var requestHead: HTTPRequestHead { get }
198+
199+
/// The maximal `TimeAmount` that is allowed to pass between reads from the Channel.
200+
var idleReadTimeout: TimeAmount? { get }
201+
202+
/// Will be called by the ChannelHandler to indicate that the request is going to be send.
203+
///
204+
/// This will be called on the Channel's EventLoop. Do **not block** during your execution!
205+
///
206+
/// - Returns: A bool indicating if the request should really be started. Return false if the request has already been cancelled.
207+
/// If the request is cancelled after this method call `executor.cancel()` to stop request execution.
208+
func willExecuteRequest(_: HTTPRequestExecutor) -> Bool
209+
210+
/// Will be called by the ChannelHandler to indicate that the request head has been sent.
211+
///
212+
/// This will be called on the Channel's EventLoop. Do **not block** during your execution!
213+
func requestHeadSent(_: HTTPRequestHead)
214+
215+
/// Start request streaming
216+
///
217+
/// This will be called on the Channel's EventLoop. Do **not block** during your execution!
218+
func startRequestBodyStream()
219+
220+
/// Pause request streaming
221+
///
222+
/// This will be called on the Channel's EventLoop. Do **not block** during your execution!
223+
func pauseRequestBodyStream()
224+
225+
/// Resume request streaming
226+
///
227+
/// This will be called on the Channel's EventLoop. Do **not block** during your execution!
228+
func resumeRequestBodyStream()
229+
230+
/// Receive a response head.
231+
///
232+
/// Please note that `receiveResponseHead` and `receiveResponseBodyPart` may
233+
/// be called in quick succession. It is the task's job to buffer those events for the user. Once all
234+
/// buffered data has been consumed the task must call `executor.demandResponseBodyStream`
235+
/// to ask for more data.
236+
func receiveResponseHead(_ head: HTTPResponseHead)
237+
238+
/// Receive a response body stream part.
239+
///
240+
/// Please note that `receiveResponseHead` and `receiveResponseBodyPart` may
241+
/// be called in quick succession. It is the task's job to buffer those events for the user. Once all
242+
/// buffered data has been consumed the task must call `executor.demandResponseBodyStream`
243+
/// to ask for more data.
244+
func receiveResponseBodyPart(_ buffer: ByteBuffer)
245+
246+
func receiveResponseEnd()
247+
248+
func fail(_ error: Error)
249+
}
250+

0 commit comments

Comments
 (0)