Skip to content

Commit 9c9e23d

Browse files
author
Clément Le Provost
committed
[refact] Share index instances across a client
Because of the search cache and the offline mode, index instances are not stateless. Therefore it is important to avoid having two instances pointing to the same index, which could cause inconsistencies and race conditions.
1 parent aa1cf3c commit 9c9e23d

File tree

4 files changed

+58
-15
lines changed

4 files changed

+58
-15
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Other improvements:
4444
- Improve cancellation of `Index.waitTask()`
4545
- Add tests for Objective-C bridging (online flavor only)
4646
- Make timeouts configurable
47+
- `Index` instances are now shared across a `Client`
4748

4849

4950
## 3.7 (2016-09-07)

Source/Client.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ public typealias CompletionHandler = (_ content: JSONObject?, _ error: Error?) -
133133
// NOTE: Not constant only for the sake of mocking during unit tests.
134134
var session: URLSession
135135

136+
/// Cache of already created indices.
137+
///
138+
/// This dictionary is used to avoid creating two instances to represent the same index, as it is (1) inefficient
139+
/// and (2) potentially harmful since instances are stateful (that's especially true of mirrored/offline indices,
140+
/// but also of online indices because of the search cache).
141+
///
142+
/// + Note: The values are zeroing weak references to avoid leaking memory when an index is no longer used.
143+
///
144+
var indices: NSMapTable<NSString, AnyObject> = NSMapTable(keyOptions: [.strongMemory], valueOptions: [.weakMemory])
145+
136146
/// Operation queue used to keep track of requests.
137147
/// `Request` instances are inherently asynchronous, since they are merely wrappers around `NSURLSessionTask`.
138148
/// The sole purpose of the queue is to retain them for the duration of their execution!
@@ -239,6 +249,26 @@ public typealias CompletionHandler = (_ content: JSONObject?, _ error: Error?) -
239249
return headers[name]
240250
}
241251

252+
/// Obtain a proxy to an Algolia index (no server call required by this method).
253+
///
254+
/// + Note: Only one instance can exist for a given index name. Subsequent calls to this method with the same
255+
/// index name will return the same instance, unless it has already been released.
256+
///
257+
/// - parameter indexName: The name of the index.
258+
/// - returns: A proxy to the specified index.
259+
///
260+
@objc(indexWithName:)
261+
public func index(withName indexName: String) -> Index {
262+
if let index = indices.object(forKey: indexName as NSString) {
263+
assert(index is Index, "An index with the same name but a different type has already been created") // may happen in offline mode
264+
return index as! Index
265+
} else {
266+
let index = Index(client: self, name: indexName)
267+
indices.setObject(index, forKey: indexName as NSString)
268+
return index
269+
}
270+
}
271+
242272
// MARK: - Operations
243273

244274
/// List existing indexes.
@@ -305,19 +335,6 @@ public typealias CompletionHandler = (_ content: JSONObject?, _ error: Error?) -
305335
return performHTTPQuery(path: path, method: .POST, body: request as [String : Any]?, hostnames: writeHosts, completionHandler: completionHandler)
306336
}
307337

308-
/// Create a proxy to an Algolia index (no server call required by this method).
309-
///
310-
/// - parameter indexName: The name of the index.
311-
/// - returns: A new proxy to the specified index.
312-
///
313-
@objc(indexWithName:)
314-
public func index(withName indexName: String) -> Index {
315-
// IMPLEMENTATION NOTE: This method is called `initIndex` in other clients, which better conveys its semantics.
316-
// However, methods prefixed by `init` are automatically considered as initializers by the Objective-C bridge.
317-
// Therefore, `initIndex` would fail to compile in Objective-C, because its return type is not `instancetype`.
318-
return Index(client: self, name: indexName)
319-
}
320-
321338
/// Strategy when running multiple queries. See `Client.multipleQueries(...)`.
322339
///
323340
public enum MultipleQueriesStrategy: String {

Source/Offline/OfflineClient.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,14 @@ import Foundation
109109
/// + Note: The offline client returns mirror-capable indices.
110110
///
111111
@objc public override func index(withName indexName: String) -> MirroredIndex {
112-
return MirroredIndex(client: self, name: indexName)
112+
if let index = indices.object(forKey: indexName as NSString) {
113+
assert(index is MirroredIndex, "An index with the same name but a different type has already been created")
114+
return index as! MirroredIndex
115+
} else {
116+
let index = MirroredIndex(client: self, name: indexName)
117+
indices.setObject(index, forKey: indexName as NSString)
118+
return index
119+
}
113120
}
114121

115122
// MARK: - Utils

Tests/ClientTests.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
//
2323

2424
import XCTest
25-
import AlgoliaSearch
25+
@testable import AlgoliaSearch
2626

2727
class ClientTests: XCTestCase {
2828
let expectationTimeout: TimeInterval = 100
@@ -419,4 +419,22 @@ class ClientTests: XCTestCase {
419419
]
420420
XCTAssertEqual(client.headers["User-Agent"], "ABC (1.2.3); DEF (4.5.6)")
421421
}
422+
423+
func testReusingIndices() {
424+
let indexName = "name"
425+
let initialCount = client.indices.count // another index is created during set up
426+
427+
autoreleasepool {
428+
// Create twice the same index and verify that it is re-used.
429+
let index1: Index? = client.index(withName: indexName)
430+
XCTAssertEqual(initialCount + 1, client.indices.count)
431+
let index2: Index? = client.index(withName: indexName)
432+
XCTAssertEqual(initialCount + 1, client.indices.count)
433+
XCTAssert(index1 === index2)
434+
}
435+
// Verify that the index instance has been destroyed.
436+
// NOTE: It may (and probably will) still be in the map, so we cannot rely on the count.
437+
let memorizedIndex = client.indices.object(forKey: indexName as NSString)
438+
XCTAssertNil(memorizedIndex)
439+
}
422440
}

0 commit comments

Comments
 (0)