Skip to content

fix HTTPCookieStorage threading issues #1182

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
Sep 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 68 additions & 36 deletions Foundation/HTTPCookieStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,34 @@ extension HTTPCookie {
*/
open class HTTPCookieStorage: NSObject {

/* both sharedStorage and sharedCookieStorages are synchronized on sharedSyncQ */
private static var sharedStorage: HTTPCookieStorage?
private static var sharedCookieStorages: [String: HTTPCookieStorage] = [:] //for group storage containers
private static let sharedSyncQ = DispatchQueue(label: "org.swift.HTTPCookieStorage.sharedSyncQ")

/* only modified in init */
private var cookieFilePath: String!
private let workQueue: DispatchQueue = DispatchQueue(label: "HTTPCookieStorage.workqueue")
var allCookies: [String: HTTPCookie]

/* synchronized on syncQ, please don't use _allCookies directly outside of init/deinit */
private var _allCookies: [String: HTTPCookie]
private var allCookies: [String: HTTPCookie] {
get {
if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what about linux?

Copy link
Contributor Author

@weissi weissi Sep 11, 2017

Choose a reason for hiding this comment

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

@phausler Linux seems to be handled in the *:

$ cat test.swift
if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
    print("yes")
} else {
    print("no")
}
$ swift test.swift
yes
$ uname -s
Linux

Choose a reason for hiding this comment

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

I agree to @weissi

dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
}
return self._allCookies
Copy link
Contributor

Choose a reason for hiding this comment

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

self. is not needed here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what's wrong with the self.? I think that's actually a very important piece of information. Otherwise it just looks like a local variable. The self. makes it very implicit that this is a class property and I believe that makes it more obvious to others that there are possible concurrency concerns.

}
set {
if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
}
self._allCookies = newValue
Copy link
Contributor

Choose a reason for hiding this comment

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

again self. is redundant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see above

}
}
private let syncQ = DispatchQueue(label: "org.swift.HTTPCookieStorage.syncQ")

private init(cookieStorageName: String) {
allCookies = [:]
_allCookies = [:]
cookieAcceptPolicy = .always
super.init()
let bundlePath = Bundle.main.bundlePath
Expand All @@ -59,9 +79,11 @@ open class HTTPCookieStorage: NSObject {
private func loadPersistedCookies() {
guard let cookies = NSMutableDictionary(contentsOfFile: cookieFilePath) else { return }
var cookies0 = _SwiftValue.fetch(cookies) as? [String: [String: Any]] ?? [:]
for key in cookies0.keys {
if let cookie = createCookie(cookies0[key]!) {
allCookies[key] = cookie
self.syncQ.sync {
Copy link
Contributor

Choose a reason for hiding this comment

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

again self. is redundant

for key in cookies0.keys {
if let cookie = createCookie(cookies0[key]!) {
allCookies[key] = cookie
}
}
}
}
Expand All @@ -87,11 +109,7 @@ open class HTTPCookieStorage: NSObject {
}

open var cookies: [HTTPCookie]? {
var theCookies: [HTTPCookie]?
workQueue.sync {
theCookies = Array(self.allCookies.values)
}
return theCookies
return Array(self.syncQ.sync { self.allCookies.values })
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto on self.

}

/*!
Expand All @@ -103,10 +121,12 @@ open class HTTPCookieStorage: NSObject {
*/
open class var shared: HTTPCookieStorage {
get {
if sharedStorage == nil {
sharedStorage = HTTPCookieStorage(cookieStorageName: "shared")
return sharedSyncQ.sync {
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks like a dispatch_once style; shouldn't that be fine as a lazy var?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

@weissi - How about open static var shared: HTTPCookieStorage?

if sharedStorage == nil {
sharedStorage = HTTPCookieStorage(cookieStorageName: "shared")
}
return sharedStorage!
}
return sharedStorage!
}
}

Expand All @@ -122,12 +142,14 @@ open class HTTPCookieStorage: NSObject {
method with the same identifier will return the same cookie storage instance.
*/
open class func sharedCookieStorage(forGroupContainerIdentifier identifier: String) -> HTTPCookieStorage {
guard let cookieStorage = sharedCookieStorages[identifier] else {
let newCookieStorage = HTTPCookieStorage(cookieStorageName: identifier)
sharedCookieStorages[identifier] = newCookieStorage
return newCookieStorage
return sharedSyncQ.sync {
guard let cookieStorage = sharedCookieStorages[identifier] else {
let newCookieStorage = HTTPCookieStorage(cookieStorageName: identifier)
sharedCookieStorages[identifier] = newCookieStorage
return newCookieStorage
}
return cookieStorage
}
return cookieStorage
}


Expand All @@ -138,7 +160,7 @@ open class HTTPCookieStorage: NSObject {
same name, domain and path, if any.
*/
open func setCookie(_ cookie: HTTPCookie) {
workQueue.sync {
self.syncQ.sync {
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto on self.

guard cookieAcceptPolicy != .never else { return }

//add or override
Expand Down Expand Up @@ -173,9 +195,13 @@ open class HTTPCookieStorage: NSObject {
}

private func updatePersistentStore() {
if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
}

Choose a reason for hiding this comment

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

Are we not supporting linux?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the * seems to match Linux

Choose a reason for hiding this comment

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

@weissi - Yup agreed.

//persist cookies
var persistDictionary: [String : [String : Any]] = [:]
let persistable = allCookies.filter { (_, value) in
let persistable = self.allCookies.filter { (_, value) in
value.expiresDate != nil &&
value.isSessionOnly == false &&
value.expiresDate!.timeIntervalSinceNow > 0
Expand All @@ -189,15 +215,23 @@ open class HTTPCookieStorage: NSObject {
_ = nsdict.write(toFile: cookieFilePath, atomically: true)
}

/*!
@method lockedDeleteCookie:
@abstract Delete the specified cookie, for internal callers already on syncQ.
*/
private func lockedDeleteCookie(_ cookie: HTTPCookie) {
let key = cookie.domain + cookie.path + cookie.name
self.allCookies.removeValue(forKey: key)
updatePersistentStore()
}

/*!
@method deleteCookie:
@abstract Delete the specified cookie
*/
open func deleteCookie(_ cookie: HTTPCookie) {
let key = cookie.domain + cookie.path + cookie.name
workQueue.sync {
self.allCookies.removeValue(forKey: key)
updatePersistentStore()
self.syncQ.sync {
self.lockedDeleteCookie(cookie)
}
}

Expand All @@ -206,13 +240,15 @@ open class HTTPCookieStorage: NSObject {
@abstract Delete all cookies from the cookie storage since the provided date.
*/
open func removeCookies(since date: Date) {
let cookiesSinceDate = allCookies.values.filter {
$0.properties![.created] as! Double > date.timeIntervalSinceReferenceDate
}
for cookie in cookiesSinceDate {
deleteCookie(cookie)
self.syncQ.sync {
let cookiesSinceDate = self.allCookies.values.filter {
$0.properties![.created] as! Double > date.timeIntervalSinceReferenceDate
}
for cookie in cookiesSinceDate {
lockedDeleteCookie(cookie)
}
updatePersistentStore()
}
updatePersistentStore()
}

/*!
Expand All @@ -226,12 +262,8 @@ open class HTTPCookieStorage: NSObject {
into a set of header fields to add to a request.
*/
open func cookies(for url: URL) -> [HTTPCookie]? {
var cookies: [HTTPCookie]?
guard let host = url.host else { return nil }
workQueue.sync {
cookies = Array(allCookies.values.filter{ $0.domain == host })
}
return cookies
return Array(self.syncQ.sync(execute: {allCookies}).values.filter{ $0.domain == host })
}

/*!
Expand Down
23 changes: 23 additions & 0 deletions TestFoundation/TestHTTPCookieStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import SwiftFoundation
import SwiftXCTest
#endif
import Dispatch

class TestHTTPCookieStorage: XCTestCase {

Expand All @@ -24,6 +25,7 @@ class TestHTTPCookieStorage: XCTestCase {

static var allTests: [(String, (TestHTTPCookieStorage) -> () throws -> Void)] {
return [
("test_sharedCookieStorageAccessedFromMultipleThreads", test_sharedCookieStorageAccessedFromMultipleThreads),
("test_BasicStorageAndRetrieval", test_BasicStorageAndRetrieval),
("test_deleteCookie", test_deleteCookie),
("test_removeCookies", test_removeCookies),
Expand All @@ -34,6 +36,27 @@ class TestHTTPCookieStorage: XCTestCase {
]
}

func test_sharedCookieStorageAccessedFromMultipleThreads() {
let q = DispatchQueue.global()
let syncQ = DispatchQueue(label: "TestHTTPCookieStorage.syncQ")
var allCookieStorages: [HTTPCookieStorage] = []
let g = DispatchGroup()
for _ in 0..<64 {
g.enter()
q.async {
let mySharedCookieStore = HTTPCookieStorage.shared
syncQ.async {
allCookieStorages.append(mySharedCookieStore)
g.leave()
}
}
}
g.wait()
let cookieStorages = syncQ.sync { allCookieStorages }
let mySharedCookieStore = HTTPCookieStorage.shared
XCTAssertTrue(cookieStorages.reduce(true, { $0 && $1 === mySharedCookieStore }), "\(cookieStorages)")
}

func test_BasicStorageAndRetrieval() {
basicStorageAndRetrieval(with: .shared)
basicStorageAndRetrieval(with: .groupContainer("test"))
Expand Down