@@ -93,54 +93,101 @@ public class HTTPClient {
9393 /// this indicate shutdown was called too early before tasks were completed or explicitly canceled.
9494 /// In general, setting this parameter to `true` should make it easier and faster to catch related programming errors.
9595 internal func syncShutdown( requiresCleanClose: Bool ) throws {
96- var closeError : Error ?
97-
98- let tasks = try self . stateLock. withLock { ( ) -> Dictionary < UUID , TaskProtocol > . Values in
99- if self . state != . upAndRunning {
100- throw HTTPClientError . alreadyShutdown
96+ if let eventLoop = MultiThreadedEventLoopGroup . currentEventLoop {
97+ preconditionFailure ( """
98+ BUG DETECTED: syncShutdown() must not be called when on an EventLoop.
99+ Calling syncShutdown() on any EventLoop can lead to deadlocks.
100+ Current eventLoop: \( eventLoop)
101+ """ )
102+ }
103+ let errorStorageLock = Lock ( )
104+ var errorStorage : Error ?
105+ let continuation = DispatchWorkItem { }
106+ self . shutdown ( requiresCleanClose: requiresCleanClose, queue: DispatchQueue ( label: " async-http-client.shutdown " ) ) { error in
107+ if let error = error {
108+ errorStorageLock. withLock {
109+ errorStorage = error
110+ }
101111 }
102- self . state = . shuttingDown
103- return self . tasks. values
112+ continuation. perform ( )
104113 }
105-
106- self . pool . prepareForClose ( )
107-
108- if !tasks . isEmpty , requiresCleanClose {
109- closeError = HTTPClientError . uncleanShutdown
114+ continuation . wait ( )
115+ try errorStorageLock . withLock {
116+ if let error = errorStorage {
117+ throw error
118+ }
110119 }
120+ }
121+
122+ /// Shuts down the client and event loop gracefully. This function is clearly an outlier in that it uses a completion
123+ /// callback instead of an EventLoopFuture. The reason for that is that NIO's EventLoopFutures will call back on an event loop.
124+ /// The virtue of this function is to shut the event loop down. To work around that we call back on a DispatchQueue
125+ /// instead.
126+ public func shutdown( queue: DispatchQueue , _ callback: @escaping ( Error ? ) -> Void ) {
127+ self . shutdown ( requiresCleanClose: false , queue: queue, callback)
128+ }
111129
130+ private func cancelTasks( _ tasks: Dictionary < UUID , TaskProtocol > . Values ) -> EventLoopFuture < Void > {
112131 for task in tasks {
113132 task. cancel ( )
114133 }
115134
116- try ? EventLoopFuture . andAllComplete ( ( tasks. map { $0. completion } ) , on: self . eventLoopGroup. next ( ) ) . wait ( )
117-
118- self . pool. syncClose ( )
135+ return EventLoopFuture . andAllComplete ( tasks. map { $0. completion } , on: self . eventLoopGroup. next ( ) )
136+ }
119137
120- do {
121- try self . stateLock. withLock {
122- switch self . eventLoopGroupProvider {
123- case . shared:
138+ private func shutdownEventLoop( queue: DispatchQueue , _ callback: @escaping ( Error ? ) -> Void ) {
139+ self . stateLock. withLock {
140+ switch self . eventLoopGroupProvider {
141+ case . shared:
142+ self . state = . shutDown
143+ callback ( nil )
144+ case . createNew:
145+ switch self . state {
146+ case . shuttingDown:
124147 self . state = . shutDown
125- return
126- case . createNew:
127- switch self . state {
128- case . shuttingDown:
129- self . state = . shutDown
130- try self . eventLoopGroup. syncShutdownGracefully ( )
131- case . shutDown, . upAndRunning:
132- assertionFailure ( " The only valid state at this point is \( State . shutDown) " )
133- }
148+ self . eventLoopGroup. shutdownGracefully ( queue: queue, callback)
149+ case . shutDown, . upAndRunning:
150+ assertionFailure ( " The only valid state at this point is \( State . shutDown) " )
134151 }
135152 }
136- } catch {
137- if closeError == nil {
138- closeError = error
153+ }
154+ }
155+
156+ private func shutdown( requiresCleanClose: Bool , queue: DispatchQueue , _ callback: @escaping ( Error ? ) -> Void ) {
157+ let result : Result < Dictionary < UUID , TaskProtocol > . Values , Error > = self . stateLock. withLock {
158+ if self . state != . upAndRunning {
159+ return . failure( HTTPClientError . alreadyShutdown)
160+ } else {
161+ self . state = . shuttingDown
162+ return . success( self . tasks. values)
139163 }
140164 }
141165
142- if let closeError = closeError {
143- throw closeError
166+ switch result {
167+ case . failure( let error) :
168+ callback ( error)
169+ case . success( let tasks) :
170+ self . pool. prepareForClose ( on: self . eventLoopGroup. next ( ) ) . whenComplete { _ in
171+ var closeError : Error ?
172+ if !tasks. isEmpty, requiresCleanClose {
173+ closeError = HTTPClientError . uncleanShutdown
174+ }
175+
176+ // we ignore errors here
177+ self . cancelTasks ( tasks) . whenComplete { _ in
178+ // we ignore errors here
179+ self . pool. close ( on: self . eventLoopGroup. next ( ) ) . whenComplete { _ in
180+ self . shutdownEventLoop ( queue: queue) { eventLoopError in
181+ // we prioritise .uncleanShutdown here
182+ if let error = closeError {
183+ callback ( error)
184+ } else {
185+ callback ( eventLoopError)
186+ }
187+ }
188+ }
189+ }
190+ }
144191 }
145192 }
146193
0 commit comments