Skip to content

Commit e050d63

Browse files
author
Clément Le Provost
authored
Add an option to choose a custom queue for completion handlers (#334)
* [refact] Rename queues This should make the code a tad more readable. * Add an option to choose a custom queue for completion handlers This required quite a bit of refactoring: - Eliminate all explicit references to the main dispatch queue and the main operation queue. This required creating a dedicated queue for aggregate operations (like “delete by query”), called `inMemoryQueue`. - Serialize the fallback logic in mixed online/offline requests. Since the completion queue can be parallel, this is required now. NOTE: I chose to expose an `OperationQueue` instead of a `DispatchQueue` because (as of iOS 8.0) you can wrap a dispatch queue inside an operation queue (using `underlyingQueue`) whereas the opposite is not true. Fixes #69.
1 parent 08e0fa6 commit e050d63

File tree

9 files changed

+167
-99
lines changed

9 files changed

+167
-99
lines changed

Source/AbstractClient.swift

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,14 +185,29 @@ internal struct HostStatus {
185185
// NOTE: Not constant only for the sake of mocking during unit tests.
186186
var session: URLSession
187187

188-
/// Operation queue used to keep track of requests.
188+
/// Operation queue used to keep track of network requests.
189189
/// `Request` instances are inherently asynchronous, since they are merely wrappers around `NSURLSessionTask`.
190-
/// The sole purpose of the queue is to retain them for the duration of their execution!
190+
/// The sole purpose of this queue is to retain them for the duration of their execution!
191191
///
192-
let requestQueue: OperationQueue
192+
/// + Warning: This queue must allow enough parallel executions to avoid stalling when the response time is high.
193+
/// Because it's merely a memory management tool, we don't want it to be the limiting factor; `URLSession`
194+
/// already has its own logic of connection pooling.
195+
///
196+
let onlineRequestQueue: OperationQueue
197+
198+
/// Maximum number of concurrent requests we allow per connection.
199+
/// This setting is used to dimension `onlineRequestQueue`.
200+
private let maxConcurrentRequestCountPerConnection = 4
193201

194-
/// Dispatch queue used to run completion handlers.
195-
internal var completionQueue = DispatchQueue.main
202+
/// Operation queue used to run completion handlers.
203+
/// Default = main queue.
204+
///
205+
/// + Note: This will affect completion handlers of all methods on all indices attached to this client.
206+
///
207+
/// + Warning: The queue is not retained by the client. You must ensure that it remains valid for the lifetime of
208+
/// the client.
209+
///
210+
@objc public weak var completionQueue = OperationQueue.main
196211

197212
// MARK: Constant
198213

@@ -234,8 +249,9 @@ internal struct HostStatus {
234249
configuration.httpAdditionalHeaders = fixedHTTPHeaders
235250
session = Foundation.URLSession(configuration: configuration)
236251

237-
requestQueue = OperationQueue()
238-
requestQueue.maxConcurrentOperationCount = 8
252+
onlineRequestQueue = OperationQueue()
253+
onlineRequestQueue.name = "AlgoliaSearch online requests"
254+
onlineRequestQueue.maxConcurrentOperationCount = configuration.httpMaximumConnectionsPerHost * maxConcurrentRequestCountPerConnection
239255

240256
super.init()
241257

@@ -333,7 +349,7 @@ internal struct HostStatus {
333349
func performHTTPQuery(path: String, method: HTTPMethod, body: JSONObject?, hostnames: [String], isSearchQuery: Bool = false, completionHandler: CompletionHandler? = nil) -> Operation {
334350
let request = newRequest(method: method, path: path, body: body, hostnames: hostnames, isSearchQuery: isSearchQuery, completion: completionHandler)
335351
request.completionQueue = self.completionQueue
336-
requestQueue.addOperation(request)
352+
onlineRequestQueue.addOperation(request)
337353
return request
338354
}
339355

Source/AsyncOperation.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,21 +114,27 @@ internal class AsyncOperationWithCompletion: AsyncOperation {
114114
/// User completion block to be called.
115115
let completion: CompletionHandler?
116116

117-
/// Dispatch queue used to execute the completion handler.
118-
var completionQueue: DispatchQueue?
117+
/// Operation queue used to execute the completion handler.
118+
weak var completionQueue: OperationQueue?
119119

120120
init(completionHandler: CompletionHandler?) {
121121
self.completion = completionHandler
122122
}
123+
124+
override func start() {
125+
// The completion queue should not be the same as this operation's queue, otherwise a deadlock will result.
126+
assert(OperationQueue.current != self.completionQueue)
127+
}
123128

129+
124130
/// Finish this operation.
125131
/// This method should be called exactly once per operation.
126132
internal func callCompletion(content: JSONObject?, error: Error?) {
127133
if _cancelled {
128134
return
129135
}
130136
if let completionQueue = completionQueue {
131-
completionQueue.async {
137+
completionQueue.addOperation {
132138
self._callCompletion(content: content, error: error)
133139
}
134140
} else {

Source/Client.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ import Foundation
4848
///
4949
var indices: NSMapTable<NSString, AnyObject> = NSMapTable(keyOptions: [.strongMemory], valueOptions: [.weakMemory])
5050

51+
/// Queue for purely in-memory operations (no I/Os).
52+
/// Typically used for aggregate, concurrent operations.
53+
///
54+
internal var inMemoryQueue: OperationQueue = OperationQueue()
55+
5156
// MARK: Initialization
5257

5358
/// Create a new Algolia Search client.
@@ -72,6 +77,7 @@ import Foundation
7277
let readHosts = [ "\(appID)-dsn.algolia.net" ] + fallbackHosts
7378
let writeHosts = [ "\(appID).algolia.net" ] + fallbackHosts
7479
super.init(appID: appID, apiKey: apiKey, readHosts: readHosts, writeHosts: writeHosts)
80+
inMemoryQueue.maxConcurrentOperationCount = onlineRequestQueue.maxConcurrentOperationCount
7581
}
7682

7783
/// Obtain a proxy to an Algolia index (no server call required by this method).

Source/Index.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,12 +331,11 @@ import Foundation
331331
let cacheKey = "\(path)_body_\(request)"
332332
if let content = searchCache?.objectForKey(cacheKey) {
333333
// We *have* to return something, so we create a simple operation.
334-
// Note that its execution will be deferred until the next iteration of the main run loop.
335334
let operation = AsyncBlockOperation(completionHandler: completionHandler) {
336335
return (content, nil)
337336
}
338337
operation.completionQueue = client.completionQueue
339-
OperationQueue.main.addOperation(operation)
338+
client.inMemoryQueue.addOperation(operation)
340339
return operation
341340
}
342341
// Otherwise, run an online query.
@@ -487,7 +486,7 @@ import Foundation
487486
@objc
488487
@discardableResult public func waitTask(withID taskID: Int, completionHandler: @escaping CompletionHandler) -> Operation {
489488
let operation = WaitOperation(index: self, taskID: taskID, completionHandler: completionHandler)
490-
operation.start()
489+
client.inMemoryQueue.addOperation(operation)
491490
return operation
492491
}
493492

@@ -553,7 +552,7 @@ import Foundation
553552
@objc
554553
@discardableResult public func deleteByQuery(_ query: Query, completionHandler: CompletionHandler? = nil) -> Operation {
555554
let operation = DeleteByQueryOperation(index: self, query: query, completionHandler: completionHandler)
556-
operation.start()
555+
client.inMemoryQueue.addOperation(operation)
557556
return operation
558557
}
559558

0 commit comments

Comments
 (0)