Skip to content

Commit 8396f77

Browse files
fix: HostIterator crash (#708)
* fix: synchronise host iterator callback to avoid bad access crash * test: add load test for retry strategy
1 parent 3784432 commit 8396f77

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed

Sources/AlgoliaSearchClient/Transport/RetryStrategy/AlgoliaRetryStrategy.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ class AlgoliaRetryStrategy: RetryStrategy {
3737
}
3838

3939
return HostIterator { [weak self] in
40-
self?.hosts.first { $0.supports(callType) && $0.isUp }
40+
guard let retryStrategy = self else { return nil }
41+
return retryStrategy.queue.sync {
42+
return retryStrategy.hosts.first { $0.supports(callType) && $0.isUp }
43+
}
4144
}
4245
}
4346
}

Tests/AlgoliaSearchClientTests/Unit/AlgoliaRetryStrategyTests.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,84 @@ class AlgoliaRetryStrategyTests: XCTestCase {
132132

133133
XCTAssertEqual(newReadHosts.next()?.url.absoluteString, "algolia3.com")
134134
}
135+
136+
/// Load test of hosts retry strategy
137+
func testLoad() {
138+
let expirationDelay: TimeInterval = 3
139+
let strategy = AlgoliaRetryStrategy(hosts: [host3, host4, host5], hostsExpirationDelay: expirationDelay)
140+
141+
let queueCount = 100
142+
// Generate `queueCount` queues with associated count of operation for each
143+
let queuesWithOpCount = (1...queueCount)
144+
.map { (queue: DispatchQueue(label: "queue\($0)"), operationCount: Int.random(in: 100...1000)) }
145+
146+
let operationQueue = OperationQueue()
147+
operationQueue.maxConcurrentOperationCount = 50
148+
149+
// Expectation of completion of all the operations from all the queues
150+
let allOperationsFinishedExpectation = expectation(description: "All operations finished")
151+
allOperationsFinishedExpectation.expectedFulfillmentCount = queuesWithOpCount.map(\.operationCount).reduce(0, +)
152+
153+
// Launch `operationCount` operation from each queue with randomized launch delay
154+
for (queue, operationCount) in queuesWithOpCount {
155+
for count in 1...operationCount {
156+
queue.asyncAfter(deadline: .now() + .milliseconds(.random(in: 10...500))) {
157+
let callType: CallType = [.read, .write].randomElement()!
158+
let operation = RetryableOperation(retryStrategy: strategy, callType: callType)
159+
operation.name = "\(queue.label) - \(count)"
160+
operation.completionBlock = {
161+
allOperationsFinishedExpectation.fulfill()
162+
}
163+
operationQueue.addOperation(operation)
164+
}
165+
}
166+
}
167+
168+
waitForExpectations(timeout: 20, handler: nil)
169+
}
135170

136171
}
172+
173+
/// Operation imitating server responses for the purpose of testing the retry strategy
174+
class RetryableOperation: Operation {
175+
176+
let retryStrategy: RetryStrategy
177+
let callType: CallType
178+
let waitQueue = DispatchQueue(label: "internal")
179+
180+
init(retryStrategy: RetryStrategy, callType: CallType) {
181+
self.retryStrategy = retryStrategy
182+
self.callType = callType
183+
super.init()
184+
}
185+
186+
override var debugDescription: String {
187+
return "\(name ?? "")"
188+
}
189+
190+
override func main() {
191+
let hostsIterator = retryStrategy.retryableHosts(for: callType)
192+
193+
while let host = hostsIterator.next() {
194+
let result = getResult()
195+
switch retryStrategy.notify(host: host, result: result) {
196+
case .failure, .success:
197+
break
198+
case .retry:
199+
continue
200+
}
201+
}
202+
}
203+
204+
func getResult() -> Result<String, Error> {
205+
return [
206+
.failure(HTTPError(statusCode: .requestTimeout, message: nil)),
207+
.failure(URLError(.badServerResponse)),
208+
.failure(InternalError()),
209+
.success("")
210+
].randomElement()!
211+
}
212+
213+
struct InternalError: Error {}
214+
215+
}

0 commit comments

Comments
 (0)