-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[URLSession](SR-4567) Fix for a race condition in URLSession #949
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 all commits
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 |
---|---|---|
|
@@ -39,7 +39,11 @@ extension URLSession { | |
let group = DispatchGroup() | ||
fileprivate var easyHandles: [_EasyHandle] = [] | ||
fileprivate var timeoutSource: _TimeoutSource? = nil | ||
|
||
|
||
//SR-4567: we need to synchronize the register/unregister commands to the epoll machinery in libdispatch | ||
fileprivate let commandQueue: DispatchQueue = DispatchQueue(label: "Register-unregister synchronization") | ||
fileprivate var cancelInProgress: DispatchSemaphore? = nil | ||
|
||
init(configuration: URLSession._Configuration, workQueue: DispatchQueue) { | ||
queue = DispatchQueue(label: "MultiHandle.isolation", target: workQueue) | ||
setupCallbacks() | ||
|
@@ -99,25 +103,33 @@ fileprivate extension URLSession._MultiHandle { | |
// through libdispatch (DispatchSource) and store the source(s) inside | ||
// a `SocketSources` which we in turn store inside libcurl's multi handle | ||
// by means of curl_multi_assign() -- we retain the object fist. | ||
let action = _SocketRegisterAction(rawValue: CFURLSessionPoll(value: what)) | ||
var socketSources = _SocketSources.from(socketSourcePtr: socketSourcePtr) | ||
if socketSources == nil && action.needsSource { | ||
let s = _SocketSources() | ||
let p = Unmanaged.passRetained(s).toOpaque() | ||
CFURLSessionMultiHandleAssign(rawHandle, socket, UnsafeMutableRawPointer(p)) | ||
socketSources = s | ||
} else if socketSources != nil && action == .unregister { | ||
// We need to release the stored pointer: | ||
if let opaque = socketSourcePtr { | ||
Unmanaged<_SocketSources>.fromOpaque(opaque).release() | ||
commandQueue.async { | ||
self.cancelInProgress?.wait() | ||
self.cancelInProgress = nil | ||
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. the assignment here is perhaps not thread safe since the cancel handler gets a reference to it captured in the tearDown method; is it possible to instead have the semaphore last as long as the containing object context that is retained past the lifespan of the execution on the queue? 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. The nil assignment is guarded by the 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. there are three parts to the state of that semaphore; nil, not nil, and the signal. It doesn't look like a huge race potential but there is still something that strikes me as a potential failure here ( hard to tell without more understanding of exactly the involved queues/threads) This isn't a blocking objection - just a word of caution (and perhaps a suggestion to add a comment). 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, I'll add a comment around this code. |
||
|
||
let action = _SocketRegisterAction(rawValue: CFURLSessionPoll(value: what)) | ||
var socketSources = _SocketSources.from(socketSourcePtr: socketSourcePtr) | ||
if socketSources == nil && action.needsSource { | ||
let s = _SocketSources() | ||
let p = Unmanaged.passRetained(s).toOpaque() | ||
CFURLSessionMultiHandleAssign(self.rawHandle, socket, UnsafeMutableRawPointer(p)) | ||
socketSources = s | ||
} else if socketSources != nil && action == .unregister { | ||
//the beginning of an unregister operation | ||
self.cancelInProgress = DispatchSemaphore(value: 0) | ||
// We need to release the stored pointer: | ||
if let opaque = socketSourcePtr { | ||
Unmanaged<_SocketSources>.fromOpaque(opaque).release() | ||
} | ||
socketSources?.tearDown(self.cancelInProgress) | ||
socketSources = nil | ||
} | ||
socketSources = nil | ||
} | ||
if let ss = socketSources { | ||
let handler = DispatchWorkItem { [weak self] in | ||
self?.performAction(for: socket) | ||
if let ss = socketSources { | ||
let handler = DispatchWorkItem { [weak self] in | ||
self?.performAction(for: socket) | ||
} | ||
ss.createSources(with: action, fileDescriptor: Int(socket), queue: self.queue, handler: handler) | ||
} | ||
ss.createSources(with: action, fileDescriptor: Int(socket), queue: queue, handler: handler) | ||
} | ||
return 0 | ||
} | ||
|
@@ -398,12 +410,18 @@ fileprivate class _SocketSources { | |
s.resume() | ||
} | ||
|
||
func tearDown() { | ||
func tearDown(_ cancelInProgress: DispatchSemaphore?) { | ||
let cancelHandler = DispatchWorkItem { | ||
//the real end of an unregister operation! | ||
cancelInProgress?.signal() | ||
} | ||
if let s = readSource { | ||
s.setCancelHandler(handler: cancelHandler) | ||
s.cancel() | ||
} | ||
readSource = nil | ||
if let s = writeSource { | ||
s.setCancelHandler(handler: cancelHandler) | ||
s.cancel() | ||
} | ||
writeSource = nil | ||
|
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.
how long could cancels take? this could back up this queue indefinitely.
Uh oh!
There was an error while loading. Please reload this page.
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.
Thanks @phausler for the review
In my experience, I haven't seen
DispatchSource.cancel()
blocking for a long time. Moreover, the intention here is to block the command queue until acancel()
goes through completely, of course assuming thatcancel()
will not block. I have considered the fact that the cancel handler runs on the same queue as the event handler. The event handler will do two things:So, in my understanding the event handler will not block the cancel handler - which means the
wait()
here will not block indefinitely.@dgrove-oss Can
DispatchSource.cancel()
can block under any other circumstances?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.
If the only work that falls out is just
DispatchSource.cancel()
this is a non issue; it effectively is an atomic assign of a value.