@@ -104,6 +104,186 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
104
104
XCTAssertNoThrow ( try requestBag. task. futureResult. wait ( ) )
105
105
}
106
106
107
+ func testWriteBackpressure( ) {
108
+ class TestWriter {
109
+ let eventLoop : EventLoop
110
+
111
+ let parts : Int
112
+
113
+ var finishFuture : EventLoopFuture < Void > { self . finishPromise. futureResult }
114
+ private let finishPromise : EventLoopPromise < Void >
115
+ private( set) var written : Int = 0
116
+
117
+ private var channelIsWritable : Bool = false
118
+
119
+ init ( eventLoop: EventLoop , parts: Int ) {
120
+ self . eventLoop = eventLoop
121
+ self . parts = parts
122
+
123
+ self . finishPromise = eventLoop. makePromise ( of: Void . self)
124
+ }
125
+
126
+ func start( writer: HTTPClient . Body . StreamWriter ) -> EventLoopFuture < Void > {
127
+ func recursive( ) {
128
+ XCTAssert ( self . eventLoop. inEventLoop)
129
+ XCTAssert ( self . channelIsWritable)
130
+ if self . written == self . parts {
131
+ self . finishPromise. succeed ( ( ) )
132
+ } else {
133
+ self . eventLoop. execute {
134
+ let future = writer. write ( . byteBuffer( . init( bytes: [ 0 , 1 ] ) ) )
135
+ self . written += 1
136
+ future. whenComplete { result in
137
+ switch result {
138
+ case . success:
139
+ recursive ( )
140
+ case . failure( let error) :
141
+ XCTFail ( " Unexpected error: \( error) " )
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ recursive ( )
149
+
150
+ return self . finishFuture
151
+ }
152
+
153
+ func writabilityChanged( _ newValue: Bool ) {
154
+ self . channelIsWritable = newValue
155
+ }
156
+ }
157
+
158
+ let embedded = EmbeddedChannel ( )
159
+ let testWriter = TestWriter ( eventLoop: embedded. eventLoop, parts: 50 )
160
+ var maybeTestUtils : HTTP1TestTools ?
161
+ XCTAssertNoThrow ( maybeTestUtils = try embedded. setupHTTP1Connection ( ) )
162
+ guard let testUtils = maybeTestUtils else { return XCTFail ( " Expected connection setup works " ) }
163
+
164
+ var maybeRequest : HTTPClient . Request ?
165
+ XCTAssertNoThrow ( maybeRequest = try HTTPClient . Request ( url: " http://localhost/ " , method: . POST, body: . stream( length: 100 ) { writer in
166
+ testWriter. start ( writer: writer)
167
+ } ) )
168
+ guard let request = maybeRequest else { return XCTFail ( " Expected to be able to create a request " ) }
169
+
170
+ let delegate = ResponseAccumulator ( request: request)
171
+ var maybeRequestBag : RequestBag < ResponseAccumulator > ?
172
+ XCTAssertNoThrow ( maybeRequestBag = try RequestBag (
173
+ request: request,
174
+ eventLoopPreference: . delegate( on: embedded. eventLoop) ,
175
+ task: . init( eventLoop: embedded. eventLoop, logger: testUtils. logger) ,
176
+ redirectHandler: nil ,
177
+ connectionDeadline: . now( ) + . seconds( 30 ) ,
178
+ idleReadTimeout: . milliseconds( 200 ) ,
179
+ delegate: delegate
180
+ ) )
181
+ guard let requestBag = maybeRequestBag else { return XCTFail ( " Expected to be able to create a request bag " ) }
182
+
183
+ // the handler only writes once the channel is writable
184
+ embedded. isWritable = false
185
+ testWriter. writabilityChanged ( false )
186
+ embedded. pipeline. fireChannelWritabilityChanged ( )
187
+ testUtils. connection. execute ( request: requestBag)
188
+
189
+ XCTAssertEqual ( try embedded. readOutbound ( as: HTTPClientRequestPart . self) , . none)
190
+
191
+ embedded. isWritable = true
192
+ testWriter. writabilityChanged ( true )
193
+ embedded. pipeline. fireChannelWritabilityChanged ( )
194
+
195
+ XCTAssertNoThrow ( try embedded. receiveHeadAndVerify {
196
+ XCTAssertEqual ( $0. method, . POST)
197
+ XCTAssertEqual ( $0. uri, " / " )
198
+ XCTAssertEqual ( $0. headers. first ( name: " host " ) , " localhost " )
199
+ XCTAssertEqual ( $0. headers. first ( name: " content-length " ) , " 100 " )
200
+ } )
201
+
202
+ // the next body write will be executed once we tick the el. before we make the channel
203
+ // unwritable
204
+
205
+ for index in 0 ..< 50 {
206
+ embedded. isWritable = false
207
+ testWriter. writabilityChanged ( false )
208
+ embedded. pipeline. fireChannelWritabilityChanged ( )
209
+
210
+ XCTAssertEqual ( testWriter. written, index)
211
+
212
+ embedded. embeddedEventLoop. run ( )
213
+
214
+ XCTAssertNoThrow ( try embedded. receiveBodyAndVerify {
215
+ XCTAssertEqual ( $0. readableBytes, 2 )
216
+ } )
217
+
218
+ XCTAssertEqual ( testWriter. written, index + 1 )
219
+
220
+ embedded. isWritable = true
221
+ testWriter. writabilityChanged ( true )
222
+ embedded. pipeline. fireChannelWritabilityChanged ( )
223
+ }
224
+
225
+ embedded. embeddedEventLoop. run ( )
226
+ XCTAssertNoThrow ( try embedded. receiveEnd ( ) )
227
+
228
+ let responseHead = HTTPResponseHead ( version: . http1_1, status: . ok)
229
+ XCTAssertNoThrow ( try embedded. writeInbound ( HTTPClientResponsePart . head ( responseHead) ) )
230
+ embedded. read ( )
231
+
232
+ XCTAssertEqual ( testUtils. connectionDelegate. hitConnectionClosed, 0 )
233
+ XCTAssertEqual ( testUtils. connectionDelegate. hitConnectionReleased, 0 )
234
+ XCTAssertNoThrow ( try embedded. writeInbound ( HTTPClientResponsePart . end ( nil ) ) )
235
+ XCTAssertEqual ( testUtils. connectionDelegate. hitConnectionClosed, 0 )
236
+ XCTAssertEqual ( testUtils. connectionDelegate. hitConnectionReleased, 1 )
237
+
238
+ XCTAssertNoThrow ( try requestBag. task. futureResult. wait ( ) )
239
+ }
240
+
241
+ func testClientHandlerCancelsRequestIfWeWantToShutdown( ) {
242
+ let embedded = EmbeddedChannel ( )
243
+ var maybeTestUtils : HTTP1TestTools ?
244
+ XCTAssertNoThrow ( maybeTestUtils = try embedded. setupHTTP1Connection ( ) )
245
+ guard let testUtils = maybeTestUtils else { return XCTFail ( " Expected connection setup works " ) }
246
+
247
+ var maybeRequest : HTTPClient . Request ?
248
+ XCTAssertNoThrow ( maybeRequest = try HTTPClient . Request ( url: " http://localhost/ " ) )
249
+ guard let request = maybeRequest else { return XCTFail ( " Expected to be able to create a request " ) }
250
+
251
+ let delegate = ResponseAccumulator ( request: request)
252
+ var maybeRequestBag : RequestBag < ResponseAccumulator > ?
253
+ XCTAssertNoThrow ( maybeRequestBag = try RequestBag (
254
+ request: request,
255
+ eventLoopPreference: . delegate( on: embedded. eventLoop) ,
256
+ task: . init( eventLoop: embedded. eventLoop, logger: testUtils. logger) ,
257
+ redirectHandler: nil ,
258
+ connectionDeadline: . now( ) + . seconds( 30 ) ,
259
+ idleReadTimeout: . milliseconds( 200 ) ,
260
+ delegate: delegate
261
+ ) )
262
+ guard let requestBag = maybeRequestBag else { return XCTFail ( " Expected to be able to create a request bag " ) }
263
+
264
+ testUtils. connection. execute ( request: requestBag)
265
+
266
+ XCTAssertNoThrow ( try embedded. receiveHeadAndVerify {
267
+ XCTAssertEqual ( $0. method, . GET)
268
+ XCTAssertEqual ( $0. uri, " / " )
269
+ XCTAssertEqual ( $0. headers. first ( name: " host " ) , " localhost " )
270
+ } )
271
+ XCTAssertNoThrow ( try embedded. receiveEnd ( ) )
272
+
273
+ XCTAssertTrue ( embedded. isActive)
274
+ XCTAssertEqual ( testUtils. connectionDelegate. hitConnectionClosed, 0 )
275
+ XCTAssertEqual ( testUtils. connectionDelegate. hitConnectionReleased, 0 )
276
+ testUtils. connection. cancel ( )
277
+ XCTAssertFalse ( embedded. isActive)
278
+ embedded. embeddedEventLoop. run ( )
279
+ XCTAssertEqual ( testUtils. connectionDelegate. hitConnectionClosed, 1 )
280
+ XCTAssertEqual ( testUtils. connectionDelegate. hitConnectionReleased, 0 )
281
+
282
+ XCTAssertThrowsError ( try requestBag. task. futureResult. wait ( ) ) {
283
+ XCTAssertEqual ( $0 as? HTTPClientError , . cancelled)
284
+ }
285
+ }
286
+
107
287
func testIdleReadTimeout( ) {
108
288
let embedded = EmbeddedChannel ( )
109
289
var maybeTestUtils : HTTP1TestTools ?
0 commit comments