@@ -17,13 +17,15 @@ import NIOConcurrencyHelpers
17
17
import NIOHTTP1
18
18
19
19
/// A barebone HTTP client to interact with AWS Runtime Engine which is an HTTP server.
20
+ /// Note that Lambda Runtime API dictate that only one requests runs at a time.
21
+ /// This means we can avoid locks and other concurrency concern we would otherwise need to build into the client
20
22
internal final class HTTPClient {
21
23
private let eventLoop : EventLoop
22
24
private let configuration : Lambda . Configuration . RuntimeEngine
23
25
private let targetHost : String
24
26
25
27
private var state = State . disconnected
26
- private let stateLock = Lock ( )
28
+ private let executing = NIOAtomic . makeAtomic ( value : false )
27
29
28
30
init ( eventLoop: EventLoop , configuration: Lambda . Configuration . RuntimeEngine ) {
29
31
self . eventLoop = eventLoop
@@ -46,38 +48,26 @@ internal final class HTTPClient {
46
48
timeout: timeout ?? self . configuration. requestTimeout) )
47
49
}
48
50
49
- private func execute( _ request: Request ) -> EventLoopFuture < Response > {
50
- self . stateLock. lock ( )
51
+ // TODO: cap reconnect attempt
52
+ private func execute( _ request: Request , validate: Bool = true ) -> EventLoopFuture < Response > {
53
+ precondition ( !validate || self . executing. compareAndExchange ( expected: false , desired: true ) , " expecting single request at a time " )
54
+
51
55
switch self . state {
52
56
case . disconnected:
53
- let promise = self . eventLoop. makePromise ( of: Response . self)
54
- self . state = . connecting( promise. futureResult)
55
- self . stateLock. unlock ( )
56
- self . connect ( ) . flatMap { channel -> EventLoopFuture < Response > in
57
- self . stateLock. withLock {
58
- guard case . connecting = self . state else {
59
- preconditionFailure ( " invalid state \( self . state) " )
60
- }
61
- self . state = . connected( channel)
62
- }
63
- return self . execute ( request)
64
- } . cascade ( to: promise)
65
- return promise. futureResult
66
- case . connecting( let future) :
67
- let future = future. flatMap { _ in
68
- self . execute ( request)
57
+ return self . connect ( ) . flatMap { channel -> EventLoopFuture < Response > in
58
+ self . state = . connected( channel)
59
+ return self . execute ( request, validate: false )
69
60
}
70
- self . state = . connecting( future)
71
- self . stateLock. unlock ( )
72
- return future
73
61
case . connected( let channel) :
74
62
guard channel. isActive else {
75
63
self . state = . disconnected
76
- self . stateLock. unlock ( )
77
- return self . execute ( request)
64
+ return self . execute ( request, validate: false )
78
65
}
79
- self . stateLock . unlock ( )
66
+
80
67
let promise = channel. eventLoop. makePromise ( of: Response . self)
68
+ promise. futureResult. whenComplete { _ in
69
+ precondition ( self . executing. compareAndExchange ( expected: true , desired: false ) , " invalid execution state " )
70
+ }
81
71
let wrapper = HTTPRequestWrapper ( request: request, promise: promise)
82
72
channel. writeAndFlush ( wrapper) . cascadeFailure ( to: promise)
83
73
return promise. futureResult
@@ -133,7 +123,6 @@ internal final class HTTPClient {
133
123
134
124
private enum State {
135
125
case disconnected
136
- case connecting( EventLoopFuture < Response > )
137
126
case connected( Channel )
138
127
}
139
128
}
@@ -213,15 +202,15 @@ private final class HTTPHandler: ChannelDuplexHandler {
213
202
}
214
203
}
215
204
216
- private final class UnaryHandler : ChannelInboundHandler , ChannelOutboundHandler {
205
+ // no need in locks since we validate only one request can run at a time
206
+ private final class UnaryHandler : ChannelDuplexHandler {
217
207
typealias OutboundIn = HTTPRequestWrapper
218
208
typealias InboundIn = HTTPClient . Response
219
209
typealias OutboundOut = HTTPClient . Request
220
210
221
211
private let keepAlive : Bool
222
212
223
- private let lock = Lock ( )
224
- private var pendingResponses = CircularBuffer < ( promise: EventLoopPromise < HTTPClient . Response > , timeout: Scheduled < Void > ? ) > ( )
213
+ private var pending : ( promise: EventLoopPromise < HTTPClient . Response > , timeout: Scheduled < Void > ? ) ?
225
214
private var lastError : Error ?
226
215
227
216
init ( keepAlive: Bool ) {
@@ -232,19 +221,20 @@ private final class UnaryHandler: ChannelInboundHandler, ChannelOutboundHandler
232
221
let wrapper = unwrapOutboundIn ( data)
233
222
let timeoutTask = wrapper. request. timeout. map {
234
223
context. eventLoop. scheduleTask ( in: $0) {
235
- if ( self . lock. withLock { !self . pendingResponses. isEmpty } ) {
236
- self . errorCaught ( context: context, error: HTTPClient . Errors. timeout)
224
+ if self . pending != nil {
225
+ // TODO: need to verify this is thread safe i.e tha the timeout event wont interleave with the normal hander events
226
+ context. pipeline. fireErrorCaught ( HTTPClient . Errors. timeout)
237
227
}
238
228
}
239
229
}
240
- self . lock . withLockVoid { pendingResponses . append ( ( promise: wrapper. promise, timeout: timeoutTask) ) }
230
+ self . pending = ( promise: wrapper. promise, timeout: timeoutTask)
241
231
context. writeAndFlush ( wrapOutboundOut ( wrapper. request) , promise: promise)
242
232
}
243
233
244
234
func channelRead( context: ChannelHandlerContext , data: NIOAny ) {
245
235
let response = unwrapInboundIn ( data)
246
- if let pending = ( self . lock . withLock { self . pendingResponses . popFirst ( ) } ) {
247
- let serverKeepAlive = response. headers [ " connection " ] . first ? . lowercased ( ) == " keep-alive "
236
+ if let pending = self . pending {
237
+ let serverKeepAlive = response. headers. first ( name : " connection " ) ? . lowercased ( ) == " keep-alive "
248
238
if !self . keepAlive || !serverKeepAlive {
249
239
pending. promise. futureResult. whenComplete { _ in
250
240
_ = context. channel. close ( )
@@ -257,20 +247,20 @@ private final class UnaryHandler: ChannelInboundHandler, ChannelOutboundHandler
257
247
258
248
func errorCaught( context: ChannelHandlerContext , error: Error ) {
259
249
// pending responses will fail with lastError in channelInactive since we are calling context.close
260
- self . lock . withLockVoid { self . lastError = error }
250
+ self . lastError = error
261
251
context. channel. close ( promise: nil )
262
252
}
263
253
264
254
func channelInactive( context: ChannelHandlerContext ) {
265
255
// fail any pending responses with last error or assume peer disconnected
266
- self . failPendingResponses ( self . lock . withLock { self . lastError } ?? HTTPClient . Errors. connectionResetByPeer)
256
+ self . failPendingResponses ( self . lastError ?? HTTPClient . Errors. connectionResetByPeer)
267
257
context. fireChannelInactive ( )
268
258
}
269
259
270
260
private func failPendingResponses( _ error: Error ) {
271
- while let pending = ( self . lock . withLock { pendingResponses . popFirst ( ) } ) {
272
- pending. 1 ? . cancel ( )
273
- pending. 0 . fail ( error)
261
+ if let pending = self . pending {
262
+ pending. timeout ? . cancel ( )
263
+ pending. promise . fail ( error)
274
264
}
275
265
}
276
266
}
0 commit comments