Merge pull request #1182 from weissi/jw-fix-httpcookiestorage-threading

diff --git a/Foundation/HTTPCookieStorage.swift b/Foundation/HTTPCookieStorage.swift
index 045b69d..2209818 100644
--- a/Foundation/HTTPCookieStorage.swift
+++ b/Foundation/HTTPCookieStorage.swift
@@ -36,14 +36,34 @@
 */
 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, *) {
+                dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
+            }
+            return self._allCookies
+        }
+        set {
+            if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
+                dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
+            }
+            self._allCookies = newValue
+        }
+    }
+    private let syncQ = DispatchQueue(label: "org.swift.HTTPCookieStorage.syncQ")
 
     private init(cookieStorageName: String) {
-        allCookies = [:]
+        _allCookies = [:]
         cookieAcceptPolicy = .always
         super.init()
         let bundlePath = Bundle.main.bundlePath
@@ -59,9 +79,11 @@
     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 {
+            for key in cookies0.keys {
+                if let cookie = createCookie(cookies0[key]!) {
+                    allCookies[key] = cookie
+                }
             }
         }
     }
@@ -87,11 +109,7 @@
     }
 
     open var cookies: [HTTPCookie]? {
-        var theCookies: [HTTPCookie]?
-        workQueue.sync {
-                theCookies = Array(self.allCookies.values)
-        }
-        return theCookies
+        return Array(self.syncQ.sync { self.allCookies.values })
     }
     
     /*!
@@ -103,10 +121,12 @@
     */
     open class var shared: HTTPCookieStorage {
         get {
-            if sharedStorage == nil {
-                sharedStorage = HTTPCookieStorage(cookieStorageName: "shared")
+            return sharedSyncQ.sync {
+                if sharedStorage == nil {
+                    sharedStorage = HTTPCookieStorage(cookieStorageName: "shared")
+                }
+                return sharedStorage!
             }
-            return sharedStorage!
         }
     }
     
@@ -122,12 +142,14 @@
         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
     }
 
     
@@ -138,7 +160,7 @@
         same name, domain and path, if any.
     */
     open func setCookie(_ cookie: HTTPCookie) {
-        workQueue.sync {
+        self.syncQ.sync {
             guard cookieAcceptPolicy != .never else { return }
 
             //add or override
@@ -173,9 +195,13 @@
     }
 
     private func updatePersistentStore() {
+        if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
+            dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
+        }
+
         //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
@@ -190,14 +216,22 @@
     }
 
     /*!
+        @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)
         }
     }
     
@@ -206,13 +240,15 @@
      @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
+        self.syncQ.sync {
+            let cookiesSinceDate = self.allCookies.values.filter {
+                $0.properties![.created] as! Double >  date.timeIntervalSinceReferenceDate
+            }
+            for cookie in cookiesSinceDate {
+                lockedDeleteCookie(cookie)
+            }
+            updatePersistentStore()
         }
-        for cookie in cookiesSinceDate {
-            deleteCookie(cookie)
-        }
-        updatePersistentStore()
     }
 
     /*!
@@ -226,12 +262,8 @@
         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 })
     }
     
     /*!
diff --git a/TestFoundation/TestHTTPCookieStorage.swift b/TestFoundation/TestHTTPCookieStorage.swift
index b195a08..f65a01e 100644
--- a/TestFoundation/TestHTTPCookieStorage.swift
+++ b/TestFoundation/TestHTTPCookieStorage.swift
@@ -14,6 +14,7 @@
     import SwiftFoundation
     import SwiftXCTest
 #endif
+import Dispatch
 
 class TestHTTPCookieStorage: XCTestCase {
 
@@ -24,6 +25,7 @@
 
     static var allTests: [(String, (TestHTTPCookieStorage) -> () throws -> Void)] {
         return [
+            ("test_sharedCookieStorageAccessedFromMultipleThreads", test_sharedCookieStorageAccessedFromMultipleThreads),
             ("test_BasicStorageAndRetrieval", test_BasicStorageAndRetrieval),
             ("test_deleteCookie", test_deleteCookie),
             ("test_removeCookies", test_removeCookies),
@@ -34,6 +36,27 @@
         ]
     }
 
+    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"))