Skip to content

Commit f64b3f6

Browse files
Merge pull request #1227 from apollographql/fix/multifetch
Fix for concurrent fetching bugs
2 parents 418dc11 + e58853d commit f64b3f6

7 files changed

Lines changed: 175 additions & 79 deletions

File tree

Apollo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
9B518C87235F819E004C426D /* CLIDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C85235F8125004C426D /* CLIDownloader.swift */; };
2727
9B518C8C235F8B5F004C426D /* ApolloFilePathHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C8A235F8B05004C426D /* ApolloFilePathHelper.swift */; };
2828
9B518C8D235F8B9E004C426D /* CLIDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B518C88235F8AD4004C426D /* CLIDownloaderTests.swift */; };
29+
9B554CC4247DC29A002F452A /* TaskData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B554CC3247DC29A002F452A /* TaskData.swift */; };
2930
9B5A1EFC243528AA00F066BB /* InputObjectGenerationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5A1EFB243528AA00F066BB /* InputObjectGenerationTests.swift */; };
3031
9B5A1EFD24352AC100F066BB /* ExpectedReviewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F06C241C646700E97318 /* ExpectedReviewInput.swift */; };
3132
9B5A1EFE24352AED00F066BB /* ExpectedColorInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F06A241C643000E97318 /* ExpectedColorInput.swift */; };
@@ -399,6 +400,7 @@
399400
9B518C85235F8125004C426D /* CLIDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIDownloader.swift; sourceTree = "<group>"; };
400401
9B518C88235F8AD4004C426D /* CLIDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIDownloaderTests.swift; sourceTree = "<group>"; };
401402
9B518C8A235F8B05004C426D /* ApolloFilePathHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloFilePathHelper.swift; sourceTree = "<group>"; };
403+
9B554CC3247DC29A002F452A /* TaskData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskData.swift; sourceTree = "<group>"; };
402404
9B5A1EE3243284F300F066BB /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
403405
9B5A1EFB243528AA00F066BB /* InputObjectGenerationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputObjectGenerationTests.swift; sourceTree = "<group>"; };
404406
9B5A1EFF2435356400F066BB /* ExpectedColorInputNoModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpectedColorInputNoModifier.swift; sourceTree = "<group>"; };
@@ -1287,6 +1289,7 @@
12871289
9F69FFA81D42855900E000B1 /* NetworkTransport.swift */,
12881290
9BEDC79D22E5D2CF00549BF6 /* RequestCreator.swift */,
12891291
9B4F453E244A27B900C2CF7D /* URLSessionClient.swift */,
1292+
9B554CC3247DC29A002F452A /* TaskData.swift */,
12901293
);
12911294
name = Network;
12921295
sourceTree = "<group>";
@@ -2112,6 +2115,7 @@
21122115
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */,
21132116
9FC9A9C51E2D6CE70023C4D5 /* GraphQLSelectionSet.swift in Sources */,
21142117
9BDE43DD22C6705300FD7C7F /* GraphQLHTTPResponseError.swift in Sources */,
2118+
9B554CC4247DC29A002F452A /* TaskData.swift in Sources */,
21152119
9FCDFD231E33A0D8007519DC /* AsynchronousOperation.swift in Sources */,
21162120
9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */,
21172121
9B708AAD2305884500604A11 /* ApolloClientProtocol.swift in Sources */,

Sources/Apollo/Atomic.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ public class Atomic<T> {
2727
_value = newValue
2828
}
2929
}
30+
31+
public func mutate(block: (inout T) -> Void) {
32+
lock.lock()
33+
block(&_value)
34+
lock.unlock()
35+
}
3036
}
3137

3238
public extension Atomic where T == Int {

Sources/Apollo/TaskData.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Foundation
2+
3+
public class TaskData {
4+
5+
public let rawCompletion: URLSessionClient.RawCompletion?
6+
public let completionBlock: URLSessionClient.Completion
7+
private(set) var data: Data = Data()
8+
private(set) var response: HTTPURLResponse? = nil
9+
10+
init(rawCompletion: URLSessionClient.RawCompletion?,
11+
completionBlock: @escaping URLSessionClient.Completion) {
12+
self.rawCompletion = rawCompletion
13+
self.completionBlock = completionBlock
14+
}
15+
16+
func append(additionalData: Data) {
17+
self.data.append(additionalData)
18+
}
19+
20+
func responseReceived(response: URLResponse) {
21+
if let httpResponse = response as? HTTPURLResponse {
22+
self.response = httpResponse
23+
}
24+
}
25+
}

Sources/Apollo/URLSessionClient.swift

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
2323
/// A completion block returning a result. On `.success` it will contain a tuple with non-nil `Data` and its corresponding `HTTPURLResponse`. On `.failure` it will contain an error.
2424
public typealias Completion = (Result<(Data, HTTPURLResponse), Error>) -> Void
2525

26-
private var completionBlocks = Atomic<[Int: Completion]>([:])
27-
private var rawCompletions = Atomic<[Int: RawCompletion]>([:])
28-
private var datas = Atomic<[Int: Data]>([:])
29-
private var responses = Atomic<[Int: HTTPURLResponse]>([:])
26+
private var tasks = Atomic<[Int: TaskData]>([:])
3027

3128
/// The raw URLSession being used for this client
3229
open private(set) var session: URLSession!
@@ -44,24 +41,22 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
4441
delegateQueue: callbackQueue)
4542
}
4643

44+
deinit {
45+
self.clearAllTasks()
46+
}
47+
4748
/// Clears underlying dictionaries of any data related to a particular task identifier.
4849
///
4950
/// - Parameter identifier: The identifier of the task to clear.
50-
open func clearTask(with identifier: Int) {
51-
self.rawCompletions.value.removeValue(forKey: identifier)
52-
self.completionBlocks.value.removeValue(forKey: identifier)
53-
self.datas.value.removeValue(forKey: identifier)
54-
self.responses.value.removeValue(forKey: identifier)
51+
open func clear(task identifier: Int) {
52+
self.tasks.mutate { $0.removeValue(forKey: identifier) }
5553
}
5654

5755
/// Clears underlying dictionaries of any data related to all tasks.
5856
///
5957
/// Mostly useful for cleanup and/or after invalidation of the `URLSession`.
6058
open func clearAllTasks() {
61-
self.rawCompletions.value.removeAll()
62-
self.completionBlocks.value.removeAll()
63-
self.datas.value.removeAll()
64-
self.responses.value.removeAll()
59+
self.tasks.mutate { $0.removeAll() }
6560
}
6661

6762
/// The main method to perform a request.
@@ -76,16 +71,15 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
7671
open func sendRequest(_ request: URLRequest,
7772
rawTaskCompletionHandler: RawCompletion? = nil,
7873
completion: @escaping Completion) -> URLSessionTask {
79-
let dataTask = self.session.dataTask(with: request)
80-
if let rawCompletion = rawTaskCompletionHandler {
81-
self.rawCompletions.value[dataTask.taskIdentifier] = rawCompletion
82-
}
74+
let task = self.session.dataTask(with: request)
75+
let taskData = TaskData(rawCompletion: rawTaskCompletionHandler,
76+
completionBlock: completion)
8377

84-
self.completionBlocks.value[dataTask.taskIdentifier] = completion
85-
self.datas.value[dataTask.taskIdentifier] = Data()
86-
dataTask.resume()
78+
self.tasks.mutate { $0[task.taskIdentifier] = taskData }
8779

88-
return dataTask
80+
task.resume()
81+
82+
return task
8983
}
9084

9185
/// Cancels a given task and clears out its underlying data.
@@ -94,16 +88,16 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
9488
///
9589
/// - Parameter task: The task you wish to cancel.
9690
open func cancel(task: URLSessionTask) {
97-
self.clearTask(with: task.taskIdentifier)
91+
self.clear(task: task.taskIdentifier)
9892
task.cancel()
9993
}
10094

10195
// MARK: - URLSessionDelegate
10296

10397
open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
10498
let finalError = error ?? URLSessionClientError.sessionBecameInvalidWithoutUnderlyingError
105-
for block in self.completionBlocks.value.values {
106-
block(.failure(finalError))
99+
for task in self.tasks.value.values {
100+
task.completionBlock(.failure(finalError))
107101
}
108102

109103
self.clearAllTasks()
@@ -145,38 +139,33 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
145139
open func urlSession(_ session: URLSession,
146140
task: URLSessionTask,
147141
didCompleteWithError error: Error?) {
148-
let taskIdentifier = task.taskIdentifier
149142
defer {
150-
self.clearTask(with: taskIdentifier)
143+
self.clear(task: task.taskIdentifier)
151144
}
152145

153-
guard let completion = self.completionBlocks.value.removeValue(forKey: taskIdentifier) else {
146+
guard let taskData = self.tasks.value[task.taskIdentifier] else {
154147
// No completion blocks, the task has likely been cancelled. Bail out.
155148
return
156149
}
157150

158-
let data = self.datas.value[taskIdentifier]
159-
let response = self.responses.value[taskIdentifier]
151+
let data = taskData.data
152+
let response = taskData.response
160153

161-
if let rawCompletion = self.rawCompletions.value.removeValue(forKey: taskIdentifier) {
154+
if let rawCompletion = taskData.rawCompletion {
162155
rawCompletion(data, response, error)
163156
}
164157

165-
guard let finalData = data else {
166-
// Data is immediately created for a task on creation, so if it's not there, something's gone wrong.
167-
completion(.failure(URLSessionClientError.dataForRequestNotFound(request: task.originalRequest)))
168-
return
169-
}
158+
let completion = taskData.completionBlock
170159

171160
if let finalError = error {
172-
completion(.failure(URLSessionClientError.networkError(data: finalData, response: response, underlying: finalError)))
161+
completion(.failure(URLSessionClientError.networkError(data: data, response: response, underlying: finalError)))
173162
} else {
174163
guard let finalResponse = response else {
175164
completion(.failure(URLSessionClientError.noHTTPResponse(request: task.originalRequest)))
176165
return
177166
}
178167

179-
completion(.success((finalData, finalResponse)))
168+
completion(.success((data, finalResponse)))
180169
}
181170
}
182171

@@ -215,7 +204,14 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
215204
open func urlSession(_ session: URLSession,
216205
dataTask: URLSessionDataTask,
217206
didReceive data: Data) {
218-
self.datas.value[dataTask.taskIdentifier]?.append(data)
207+
self.tasks.mutate {
208+
guard let taskData = $0[dataTask.taskIdentifier] else {
209+
assertionFailure("No data found for task \(dataTask.taskIdentifier), cannot append received data")
210+
return
211+
}
212+
213+
taskData.append(additionalData: data)
214+
}
219215
}
220216

221217
@available(iOS 9.0, OSXApplicationExtension 10.11, OSX 10.11, *)
@@ -246,8 +242,12 @@ open class URLSessionClient: NSObject, URLSessionDelegate, URLSessionTaskDelegat
246242
completionHandler(.allow)
247243
}
248244

249-
if let httpResponse = response as? HTTPURLResponse {
250-
self.responses.value[dataTask.taskIdentifier] = httpResponse
245+
self.tasks.mutate {
246+
guard let taskData = $0[dataTask.taskIdentifier] else {
247+
return
248+
}
249+
250+
taskData.responseReceived(response: response)
251251
}
252252
}
253253
}

Tests/ApolloCacheDependentTests/StarWarsServerTests.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ protocol TestConfig {
99
}
1010

1111
class DefaultConfig: TestConfig {
12+
let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!)
1213
func network() -> HTTPNetworkTransport {
13-
return HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!)
14+
return transport
1415
}
1516
}
1617

1718
class APQsConfig: TestConfig {
19+
let transport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!,
20+
enableAutoPersistedQueries: true)
1821
func network() -> HTTPNetworkTransport {
19-
return HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!,
20-
enableAutoPersistedQueries: true)
22+
return transport
2123
}
2224
}
2325

Tests/ApolloTests/HTTPBinAPI.swift

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,59 @@
11
import Foundation
22

33
enum HTTPBinAPI {
4-
static let baseURL = URL(string: "https://httpbin.org/")!
5-
enum Endpoint {
6-
case bytes(count: Int)
7-
case get
8-
case headers
9-
case image
10-
case post
11-
12-
var toString: String {
13-
14-
switch self {
15-
case .bytes(let count):
16-
return "bytes/\(count)"
17-
case .get:
18-
return "get"
19-
case .headers:
20-
return "headers"
21-
case .image:
22-
return "image/jpeg"
23-
case .post:
24-
return "post"
25-
}
26-
}
27-
28-
var toURL: URL {
29-
HTTPBinAPI.baseURL.appendingPathComponent(self.toString)
30-
}
4+
static let baseURL = URL(string: "https://httpbin.org")!
5+
enum Endpoint {
6+
case bytes(count: Int)
7+
case get
8+
case getWithIndex(index: Int)
9+
case headers
10+
case image
11+
case post
12+
13+
var toString: String {
14+
15+
switch self {
16+
case .bytes(let count):
17+
return "bytes/\(count)"
18+
case .get,
19+
.getWithIndex:
20+
return "get"
21+
case .headers:
22+
return "headers"
23+
case .image:
24+
return "image/jpeg"
25+
case .post:
26+
return "post"
27+
}
3128
}
32-
}
33-
34-
struct HTTPBinResponse: Codable {
3529

36-
let headers: [String: String]
37-
let url: String
38-
let json: [String: String]?
30+
var queryParams: [URLQueryItem]? {
31+
switch self {
32+
case .getWithIndex(let index):
33+
return [URLQueryItem(name: "index", value: "\(index)")]
34+
default:
35+
return nil
36+
}
37+
}
3938

40-
init(data: Data) throws {
41-
self = try JSONDecoder().decode(Self.self, from: data)
39+
var toURL: URL {
40+
var components = URLComponents(url: HTTPBinAPI.baseURL, resolvingAgainstBaseURL: false)!
41+
components.path = "/\(self.toString)"
42+
components.queryItems = self.queryParams
43+
44+
return components.url!
4245
}
46+
}
47+
}
48+
49+
struct HTTPBinResponse: Codable {
50+
51+
let headers: [String: String]
52+
let url: String
53+
let json: [String: String]?
54+
let args: [String: String]?
55+
56+
init(data: Data) throws {
57+
self = try JSONDecoder().decode(Self.self, from: data)
58+
}
4359
}

0 commit comments

Comments
 (0)