-
Notifications
You must be signed in to change notification settings - Fork 125
Implement asynchronous shutdown #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
a920ff2
6f06db5
aa0bc49
f62e798
4c5b9cc
f2ed583
e8f5a9d
f2ee93e
7b07de6
ed17f2b
df34649
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -93,54 +93,105 @@ public class HTTPClient { | |
/// this indicate shutdown was called too early before tasks were completed or explicitly canceled. | ||
/// In general, setting this parameter to `true` should make it easier and faster to catch related programming errors. | ||
internal func syncShutdown(requiresCleanClose: Bool) throws { | ||
var closeError: Error? | ||
|
||
let tasks = try self.stateLock.withLock { () -> Dictionary<UUID, TaskProtocol>.Values in | ||
if self.state != .upAndRunning { | ||
throw HTTPClientError.alreadyShutdown | ||
if let eventLoop = MultiThreadedEventLoopGroup.currentEventLoop { | ||
preconditionFailure(""" | ||
BUG DETECTED: syncShutdown() must not be called when on an EventLoop. | ||
Calling syncShutdown() on any EventLoop can lead to deadlocks. | ||
Current eventLoop: \(eventLoop) | ||
""") | ||
} | ||
let errorStorageLock = Lock() | ||
var errorStorage: Error? | ||
let continuation = DispatchWorkItem {} | ||
Lukasa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.shutdown(requiresCleanClose: requiresCleanClose, queue: .global()) { error in | ||
if let error = error { | ||
errorStorageLock.withLock { | ||
errorStorage = error | ||
} | ||
} | ||
self.state = .shuttingDown | ||
return self.tasks.values | ||
continuation.perform() | ||
} | ||
continuation.wait() | ||
try errorStorageLock.withLock { | ||
if let error = errorStorage { | ||
throw error | ||
} | ||
} | ||
} | ||
|
||
self.pool.prepareForClose() | ||
/// Shuts down the client and event loop gracefully. This function is clearly an outlier in that it uses a completion | ||
/// callback instead of an EventLoopFuture. The reason for that is that NIO's EventLoopFutures will call back on an event loop. | ||
/// The virtue of this function is to shut the event loop down. To work around that we call back on a DispatchQueue | ||
/// instead. | ||
public func shutdown(_ callback: @escaping (Error?) -> Void) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don’t default to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure, one question is though: should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (done) |
||
self.shutdown(requiresCleanClose: false, queue: .global(), callback) | ||
} | ||
|
||
if !tasks.isEmpty, requiresCleanClose { | ||
closeError = HTTPClientError.uncleanShutdown | ||
} | ||
public func shutdown(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) { | ||
self.shutdown(requiresCleanClose: false, queue: queue, callback) | ||
} | ||
|
||
private func cancelTasks(_ tasks: Dictionary<UUID, TaskProtocol>.Values) -> EventLoopFuture<Void> { | ||
for task in tasks { | ||
task.cancel() | ||
} | ||
|
||
try? EventLoopFuture.andAllComplete((tasks.map { $0.completion }), on: self.eventLoopGroup.next()).wait() | ||
|
||
self.pool.syncClose() | ||
return EventLoopFuture.andAllComplete(tasks.map { $0.completion }, on: self.eventLoopGroup.next()) | ||
} | ||
|
||
do { | ||
try self.stateLock.withLock { | ||
switch self.eventLoopGroupProvider { | ||
case .shared: | ||
private func shutdownEventLoop(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) { | ||
self.stateLock.withLock { | ||
switch self.eventLoopGroupProvider { | ||
case .shared: | ||
self.state = .shutDown | ||
callback(nil) | ||
case .createNew: | ||
switch self.state { | ||
case .shuttingDown: | ||
self.state = .shutDown | ||
return | ||
case .createNew: | ||
switch self.state { | ||
case .shuttingDown: | ||
self.state = .shutDown | ||
try self.eventLoopGroup.syncShutdownGracefully() | ||
case .shutDown, .upAndRunning: | ||
assertionFailure("The only valid state at this point is \(State.shutDown)") | ||
} | ||
self.eventLoopGroup.shutdownGracefully(queue: queue, callback) | ||
case .shutDown, .upAndRunning: | ||
assertionFailure("The only valid state at this point is \(State.shutDown)") | ||
} | ||
} | ||
} catch { | ||
if closeError == nil { | ||
closeError = error | ||
} | ||
} | ||
|
||
private func shutdown(requiresCleanClose: Bool, queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) { | ||
let result: Result<Dictionary<UUID, TaskProtocol>.Values, Error> = self.stateLock.withLock { | ||
if self.state != .upAndRunning { | ||
return .failure(HTTPClientError.alreadyShutdown) | ||
} else { | ||
self.state = .shuttingDown | ||
return .success(self.tasks.values) | ||
} | ||
} | ||
|
||
if let closeError = closeError { | ||
throw closeError | ||
switch result { | ||
case .failure(let error): | ||
callback(error) | ||
case .success(let tasks): | ||
self.pool.prepareForClose(on: self.eventLoopGroup.next()).whenComplete { _ in | ||
var closeError: Error? | ||
if !tasks.isEmpty, requiresCleanClose { | ||
closeError = HTTPClientError.uncleanShutdown | ||
} | ||
|
||
// we ignore errors here | ||
self.cancelTasks(tasks).whenComplete { _ in | ||
// we ignore errors here | ||
self.pool.close(on: self.eventLoopGroup.next()).whenComplete { _ in | ||
self.shutdownEventLoop(queue: queue) { eventLoopError in | ||
// we prioritise .uncleanShutdown here | ||
if let error = closeError { | ||
callback(error) | ||
} else { | ||
callback(eventLoopError) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure: if the check is delayed there might be cases where the assertion would have triggered but does not anymore. Even if true, I don't know how much important it is because the order/synchronization of the close actions will need to be improved anyway as we are already aware of issues linked to this in #175 and #176
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll look into that when I'll get to those issues, thanks!