Skip to content

Implementation of URLProtocol and refactoring URLSession #968

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

Merged
merged 1 commit into from
May 31, 2017
Merged

Implementation of URLProtocol and refactoring URLSession #968

merged 1 commit into from
May 31, 2017

Conversation

nethraravindran
Copy link
Contributor

Currently URLSessionTask is tightly coupled with HTTP/HTTPS specific functions. This makes it difficult to introduce support for other native and custom protocols. Refactoring URLSession helps in adding support for other native protocols like ftp and data as well. Also, URLProtocol implementation provides support for custom protocols.

This document provides the proposal for restructuring URLSession
https://gist.github.com/pushkarnk/1bae3167d2e62b09ba0dd4b0dd285179

open class func registerClass(_ protocolClass: AnyClass) -> Bool {
if protocolClass is URLProtocol.Type {
guard !_registeredProtocolClasses.contains(where: { $0 === protocolClass }) else { return true }
_lock.lock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to go before the guard otherwise threads can still race?

}

// TODO: Synchronize all the class funcs that use the registered protocol classes array
internal class func getProtocolClass(for urlRequest: URLRequest) -> AnyClass? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locking needed here too.



private static var _registeredProtocolClasses = [AnyClass]()
private static var _lock = NSLock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps call this _classesLock?

@@ -80,7 +80,7 @@ XCTMain([
testCase(TestNSURLResponse.allTests),
testCase(TestNSHTTPURLResponse.allTests),
//Disabling because of https://bugs.swift.org/browse/SR-4655 and https://bugs.swift.org/browse/SR-4647
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be removed if we are enabling the tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should re-enable the tests under this PR.

}
}

func urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Will it be possible to factor out some code this function shares with urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error)?

open class func unregisterClass(_ protocolClass: AnyClass) { NSUnimplemented() }
open class func unregisterClass(_ protocolClass: AnyClass) {
if let idx = _registeredProtocolClasses.index(where: { $0 === protocolClass }) {
_classesLock.lock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lock acquisition needs to be raised above the if, given that _registeredProtocolClasses may be concurrently accessed

@@ -219,6 +219,11 @@ open class URLSession : NSObject {
let c = URLSession._Configuration(URLSessionConfiguration: configuration)
self._configuration = c
self.multiHandle = _MultiHandle(configuration: c, workQueue: workQueue)
// registering all the protocol classes with URLProtocol
let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we want to register the native protocol classes every time a new URLSession is created. This can be a one-time activity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR: #1030

@@ -234,6 +239,11 @@ open class URLSession : NSObject {
let c = URLSession._Configuration(URLSessionConfiguration: configuration)
self._configuration = c
self.multiHandle = _MultiHandle(configuration: c, workQueue: workQueue)
// registering all the protocol classes with URLProtocol
let _ = URLProtocol.registerClass(_HTTPURLProtocol.self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we want to register the native protocol classes every time a new URLSession is created. This can be a one-time activity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR: #1030

self.easyHandle = _EasyHandle(delegate: self)
if let urlProtocolClass = URLProtocol.getProtocolClass(for: request) {
if urlProtocolClass is URLProtocol.Type {
self._protocol = (urlProtocolClass as! URLProtocol.Type).init(task: self, cachedResponse: nil, client: nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we separate the downcast on a different line of code ? Though this is fine, I'm for better readability!

@@ -149,14 +117,20 @@ open class URLSessionTask : NSObject, NSCopying {
set { taskAttributesIsolation.async(flags: .barrier) { self._response = newValue } }
}
fileprivate var _response: URLResponse? = nil
internal var setResponse : URLResponse? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we call this simply response ?

suspend()
} else {
self.internalState = .transferFailed
completeTask(withError: (self.task?.error)!)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to avoid this unguarded unwrapping

guard let url = request.url else { fatalError("No URL in request.") }

self.internalState = .transferReady(createTransferState(url: url, workQueue: (task?.workQueue)!))
if (task?.suspendCount)! < 1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be possible to avoid this unchecked unwrapping?

@pushkarnk
Copy link
Member

pushkarnk commented Apr 27, 2017

@nethraravindran Since some new files have been created, I guess you'd need to update the Foundation.xcodeproj file and include those changes as well.


func urlProtocolDidFinishLoading(_ protocol: URLProtocol) {
guard let session = `protocol`.task?.session as? URLSession else { fatalError() }
switch session.behaviour(for: `protocol`.task!) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be we should set a guard for protocol.task and generate a fatalError if its is nil? We can then access task in the remainder of the method without worries of nil :)

@pushkarnk
Copy link
Member

@swift-ci please test

@pushkarnk
Copy link
Member

@swift-ci test

@ianpartridge
Copy link
Contributor

@parkera We think this is ready for merge. Can you give your view? Thanks.

@parkera
Copy link
Contributor

parkera commented May 4, 2017

If you're happy with it, go ahead.

if protocolClass is URLProtocol.Type {
_classesLock.lock()
guard !_registeredProtocolClasses.contains(where: { $0 === protocolClass }) else {
_classesLock.unlock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use

defer {
    _classesLock.unlock()
}

here and in the following cases, otherwise it looks quite easy that a further refactoring forgets to unlock...

Copy link
Member

@pushkarnk pushkarnk May 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remembered what @parkera had to say about this :)
#902 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very surprising but fair enough, thanks for pointing that out 👍

@ianpartridge
Copy link
Contributor

Thanks @parkera we're still looking at the URLSession test failures that showed up in the CI so will wait until we understand those a bit better.

//set the request timeout
//TODO: the timeout value needs to be reset on every data transfer

var timeoutInterval = Int(httpSession.configuration.timeoutIntervalForRequest) * 1000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This timeout logic needs to be moved to URLSessionTask since it is not something specific to HTTP. We can do this in a separate PR though.

fileprivate let body: _Body
fileprivate let tempFileURL: URL
internal var suspendCount = 1
internal var totalDownloaded = 0
Copy link
Member

@pushkarnk pushkarnk May 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is specific to the http download tasks. We'd need to consider moving it to the HTTP implementation in future.

Copy link
Contributor Author

@nethraravindran nethraravindran Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR: #1029

@nethraravindran
Copy link
Contributor Author

Rebased to the latest master and fixed few bugs. Ready for test and merge.

@pushkarnk
Copy link
Member

@swift-ci please test

@pushkarnk
Copy link
Member

cc @nethraravindran build failures due to some compilation errors and warning

@pushkarnk
Copy link
Member

@swift-ci please test

1 similar comment
@pushkarnk
Copy link
Member

@swift-ci please test

@pushkarnk
Copy link
Member

Another TODO: There's a lot of places where we're doing optional chaining. I understand that we will not have nil values at these points under normal circumstances. But for those abnormal nils that may come up once in a while, we have to define clear code paths (may be a guard withfatalError()).

Since the tests are passing and the code changes are big, I am fine with merging this now. But it does warrant some future improvement work. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants