Skip to content

Commit 6a1f875

Browse files
authored
Merge pull request #1182 from weissi/jw-fix-httpcookiestorage-threading
2 parents ee61823 + 19f61e9 commit 6a1f875

File tree

2 files changed

+91
-36
lines changed

2 files changed

+91
-36
lines changed

Foundation/HTTPCookieStorage.swift

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,34 @@ extension HTTPCookie {
3636
*/
3737
open class HTTPCookieStorage: NSObject {
3838

39+
/* both sharedStorage and sharedCookieStorages are synchronized on sharedSyncQ */
3940
private static var sharedStorage: HTTPCookieStorage?
4041
private static var sharedCookieStorages: [String: HTTPCookieStorage] = [:] //for group storage containers
42+
private static let sharedSyncQ = DispatchQueue(label: "org.swift.HTTPCookieStorage.sharedSyncQ")
43+
44+
/* only modified in init */
4145
private var cookieFilePath: String!
42-
private let workQueue: DispatchQueue = DispatchQueue(label: "HTTPCookieStorage.workqueue")
43-
var allCookies: [String: HTTPCookie]
46+
47+
/* synchronized on syncQ, please don't use _allCookies directly outside of init/deinit */
48+
private var _allCookies: [String: HTTPCookie]
49+
private var allCookies: [String: HTTPCookie] {
50+
get {
51+
if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
52+
dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
53+
}
54+
return self._allCookies
55+
}
56+
set {
57+
if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
58+
dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
59+
}
60+
self._allCookies = newValue
61+
}
62+
}
63+
private let syncQ = DispatchQueue(label: "org.swift.HTTPCookieStorage.syncQ")
4464

4565
private init(cookieStorageName: String) {
46-
allCookies = [:]
66+
_allCookies = [:]
4767
cookieAcceptPolicy = .always
4868
super.init()
4969
let bundlePath = Bundle.main.bundlePath
@@ -59,9 +79,11 @@ open class HTTPCookieStorage: NSObject {
5979
private func loadPersistedCookies() {
6080
guard let cookies = NSMutableDictionary(contentsOfFile: cookieFilePath) else { return }
6181
var cookies0 = _SwiftValue.fetch(cookies) as? [String: [String: Any]] ?? [:]
62-
for key in cookies0.keys {
63-
if let cookie = createCookie(cookies0[key]!) {
64-
allCookies[key] = cookie
82+
self.syncQ.sync {
83+
for key in cookies0.keys {
84+
if let cookie = createCookie(cookies0[key]!) {
85+
allCookies[key] = cookie
86+
}
6587
}
6688
}
6789
}
@@ -87,11 +109,7 @@ open class HTTPCookieStorage: NSObject {
87109
}
88110

89111
open var cookies: [HTTPCookie]? {
90-
var theCookies: [HTTPCookie]?
91-
workQueue.sync {
92-
theCookies = Array(self.allCookies.values)
93-
}
94-
return theCookies
112+
return Array(self.syncQ.sync { self.allCookies.values })
95113
}
96114

97115
/*!
@@ -103,10 +121,12 @@ open class HTTPCookieStorage: NSObject {
103121
*/
104122
open class var shared: HTTPCookieStorage {
105123
get {
106-
if sharedStorage == nil {
107-
sharedStorage = HTTPCookieStorage(cookieStorageName: "shared")
124+
return sharedSyncQ.sync {
125+
if sharedStorage == nil {
126+
sharedStorage = HTTPCookieStorage(cookieStorageName: "shared")
127+
}
128+
return sharedStorage!
108129
}
109-
return sharedStorage!
110130
}
111131
}
112132

@@ -122,12 +142,14 @@ open class HTTPCookieStorage: NSObject {
122142
method with the same identifier will return the same cookie storage instance.
123143
*/
124144
open class func sharedCookieStorage(forGroupContainerIdentifier identifier: String) -> HTTPCookieStorage {
125-
guard let cookieStorage = sharedCookieStorages[identifier] else {
126-
let newCookieStorage = HTTPCookieStorage(cookieStorageName: identifier)
127-
sharedCookieStorages[identifier] = newCookieStorage
128-
return newCookieStorage
145+
return sharedSyncQ.sync {
146+
guard let cookieStorage = sharedCookieStorages[identifier] else {
147+
let newCookieStorage = HTTPCookieStorage(cookieStorageName: identifier)
148+
sharedCookieStorages[identifier] = newCookieStorage
149+
return newCookieStorage
150+
}
151+
return cookieStorage
129152
}
130-
return cookieStorage
131153
}
132154

133155

@@ -138,7 +160,7 @@ open class HTTPCookieStorage: NSObject {
138160
same name, domain and path, if any.
139161
*/
140162
open func setCookie(_ cookie: HTTPCookie) {
141-
workQueue.sync {
163+
self.syncQ.sync {
142164
guard cookieAcceptPolicy != .never else { return }
143165

144166
//add or override
@@ -173,9 +195,13 @@ open class HTTPCookieStorage: NSObject {
173195
}
174196

175197
private func updatePersistentStore() {
198+
if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
199+
dispatchPrecondition(condition: DispatchPredicate.onQueue(self.syncQ))
200+
}
201+
176202
//persist cookies
177203
var persistDictionary: [String : [String : Any]] = [:]
178-
let persistable = allCookies.filter { (_, value) in
204+
let persistable = self.allCookies.filter { (_, value) in
179205
value.expiresDate != nil &&
180206
value.isSessionOnly == false &&
181207
value.expiresDate!.timeIntervalSinceNow > 0
@@ -189,15 +215,23 @@ open class HTTPCookieStorage: NSObject {
189215
_ = nsdict.write(toFile: cookieFilePath, atomically: true)
190216
}
191217

218+
/*!
219+
@method lockedDeleteCookie:
220+
@abstract Delete the specified cookie, for internal callers already on syncQ.
221+
*/
222+
private func lockedDeleteCookie(_ cookie: HTTPCookie) {
223+
let key = cookie.domain + cookie.path + cookie.name
224+
self.allCookies.removeValue(forKey: key)
225+
updatePersistentStore()
226+
}
227+
192228
/*!
193229
@method deleteCookie:
194230
@abstract Delete the specified cookie
195231
*/
196232
open func deleteCookie(_ cookie: HTTPCookie) {
197-
let key = cookie.domain + cookie.path + cookie.name
198-
workQueue.sync {
199-
self.allCookies.removeValue(forKey: key)
200-
updatePersistentStore()
233+
self.syncQ.sync {
234+
self.lockedDeleteCookie(cookie)
201235
}
202236
}
203237

@@ -206,13 +240,15 @@ open class HTTPCookieStorage: NSObject {
206240
@abstract Delete all cookies from the cookie storage since the provided date.
207241
*/
208242
open func removeCookies(since date: Date) {
209-
let cookiesSinceDate = allCookies.values.filter {
210-
$0.properties![.created] as! Double > date.timeIntervalSinceReferenceDate
211-
}
212-
for cookie in cookiesSinceDate {
213-
deleteCookie(cookie)
243+
self.syncQ.sync {
244+
let cookiesSinceDate = self.allCookies.values.filter {
245+
$0.properties![.created] as! Double > date.timeIntervalSinceReferenceDate
246+
}
247+
for cookie in cookiesSinceDate {
248+
lockedDeleteCookie(cookie)
249+
}
250+
updatePersistentStore()
214251
}
215-
updatePersistentStore()
216252
}
217253

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

237269
/*!

TestFoundation/TestHTTPCookieStorage.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import SwiftFoundation
1515
import SwiftXCTest
1616
#endif
17+
import Dispatch
1718

1819
class TestHTTPCookieStorage: XCTestCase {
1920

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

2526
static var allTests: [(String, (TestHTTPCookieStorage) -> () throws -> Void)] {
2627
return [
28+
("test_sharedCookieStorageAccessedFromMultipleThreads", test_sharedCookieStorageAccessedFromMultipleThreads),
2729
("test_BasicStorageAndRetrieval", test_BasicStorageAndRetrieval),
2830
("test_deleteCookie", test_deleteCookie),
2931
("test_removeCookies", test_removeCookies),
@@ -34,6 +36,27 @@ class TestHTTPCookieStorage: XCTestCase {
3436
]
3537
}
3638

39+
func test_sharedCookieStorageAccessedFromMultipleThreads() {
40+
let q = DispatchQueue.global()
41+
let syncQ = DispatchQueue(label: "TestHTTPCookieStorage.syncQ")
42+
var allCookieStorages: [HTTPCookieStorage] = []
43+
let g = DispatchGroup()
44+
for _ in 0..<64 {
45+
g.enter()
46+
q.async {
47+
let mySharedCookieStore = HTTPCookieStorage.shared
48+
syncQ.async {
49+
allCookieStorages.append(mySharedCookieStore)
50+
g.leave()
51+
}
52+
}
53+
}
54+
g.wait()
55+
let cookieStorages = syncQ.sync { allCookieStorages }
56+
let mySharedCookieStore = HTTPCookieStorage.shared
57+
XCTAssertTrue(cookieStorages.reduce(true, { $0 && $1 === mySharedCookieStore }), "\(cookieStorages)")
58+
}
59+
3760
func test_BasicStorageAndRetrieval() {
3861
basicStorageAndRetrieval(with: .shared)
3962
basicStorageAndRetrieval(with: .groupContainer("test"))

0 commit comments

Comments
 (0)