Skip to content

Commit 2e12ddf

Browse files
author
Martin
authored
Merge pull request #69 from pace/Implement-exponential-backoff-ios
Implement exponential backoff
2 parents 0e72ea1 + 6a47c9d commit 2e12ddf

File tree

1 file changed

+96
-33
lines changed

1 file changed

+96
-33
lines changed

Templates/Swift/Common/APIClient.swift

Lines changed: 96 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ public class {{ options.name }}Client {
2525

2626
public var decodingQueue = DispatchQueue(label: "{{ options.name }}Client", qos: .utility, attributes: .concurrent)
2727

28+
/// The maximum number a request will be retried due to `401` responses
2829
public var maxUnauthorizedRetryCount = 1
2930

31+
/// The maximum number a request will be retried due to network connection errors and timeouts
32+
public var maxRetryCount = 8
33+
3034
public init(baseURL: String, configuration: URLSessionConfiguration = .default, defaultHeaders: [String: String] = [:], behaviours: [{{ options.name }}RequestBehaviour] = []) {
3135
self.baseURL = baseURL
3236
self.behaviours = self.behaviours + behaviours
@@ -41,11 +45,18 @@ public class {{ options.name }}Client {
4145
/// - Parameters:
4246
/// - request: The API request to make
4347
/// - behaviours: A list of behaviours that will be run for this request. Merged with APIClient.behaviours
48+
/// - currentUnauthorizedRetryCount: The current number of retries for this request due to `401` responses
49+
/// - currentRetryCount: The current number of retries for this request due to network connection errors and timeouts
4450
/// - completionQueue: The queue that complete will be called on
4551
/// - complete: A closure that gets passed the {{ options.name }}Response
4652
/// - Returns: A cancellable request. Not that cancellation will only work after any validation RequestBehaviours have run
4753
@discardableResult
48-
public func makeRequest<T>(_ request: {{ options.name }}Request<T>, behaviours: [{{ options.name }}RequestBehaviour] = [], currentNumberOfRetries: Int = 0, completionQueue: DispatchQueue = DispatchQueue.main, complete: @escaping ({{ options.name }}Response<T>) -> Void) -> Cancellable{{ options.name }}Request? {
54+
public func makeRequest<T>(_ request: {{ options.name }}Request<T>,
55+
behaviours: [{{ options.name }}RequestBehaviour] = [],
56+
currentUnauthorizedRetryCount: Int = 0,
57+
currentRetryCount: Int = 0,
58+
completionQueue: DispatchQueue = DispatchQueue.main,
59+
complete: @escaping ({{ options.name }}Response<T>) -> Void) -> Cancellable{{ options.name }}Request? {
4960
// create composite behaviour to make it easy to call functions on array of behaviours
5061
let requestBehaviour = {{ options.name }}RequestBehaviourGroup(request: request, behaviours: self.behaviours + behaviours)
5162

@@ -76,7 +87,13 @@ public class {{ options.name }}Client {
7687
requestBehaviour.validate(urlRequest) { result in
7788
switch result {
7889
case .success(let urlRequest):
79-
self.makeNetworkRequest(request: request, urlRequest: urlRequest, cancellableRequest: cancellableRequest, requestBehaviour: requestBehaviour, currentNumberOfRetries: currentNumberOfRetries, completionQueue: completionQueue, complete: complete)
90+
self.makeNetworkRequest(request: request,
91+
urlRequest: urlRequest,
92+
cancellableRequest: cancellableRequest,
93+
requestBehaviour: requestBehaviour,
94+
currentUnauthorizedRetryCount: currentUnauthorizedRetryCount,
95+
currentRetryCount: currentRetryCount,
96+
completionQueue: completionQueue, complete: complete)
8097
case .failure(let error):
8198
let error = APIClientError.validationError(error)
8299
let response = {{ options.name }}Response<T>(request: request, result: .failure(error), urlRequest: urlRequest)
@@ -87,7 +104,14 @@ public class {{ options.name }}Client {
87104
return cancellableRequest
88105
}
89106

90-
private func makeNetworkRequest<T>(request: {{ options.name }}Request<T>, urlRequest: URLRequest, cancellableRequest: Cancellable{{ options.name }}Request, requestBehaviour: {{ options.name }}RequestBehaviourGroup, currentNumberOfRetries: Int, completionQueue: DispatchQueue, complete: @escaping ({{ options.name }}Response<T>) -> Void) {
107+
private func makeNetworkRequest<T>(request: {{ options.name }}Request<T>,
108+
urlRequest: URLRequest,
109+
cancellableRequest: Cancellable{{ options.name }}Request,
110+
requestBehaviour: {{ options.name }}RequestBehaviourGroup,
111+
currentUnauthorizedRetryCount: Int,
112+
currentRetryCount: Int,
113+
completionQueue: DispatchQueue,
114+
complete: @escaping ({{ options.name }}Response<T>) -> Void) {
91115
requestBehaviour.beforeSend()
92116
if request.service.isUpload {
93117
let body = NSMutableData()
@@ -141,47 +165,80 @@ public class {{ options.name }}Client {
141165
}
142166
body.appendString("--\(boundary)--\r\n")
143167
} else {
144-
let task = self.session.dataTask(with: urlRequest, completionHandler: { [weak self] data, response, error -> Void in
145-
// Handle response
146-
self?.decodingQueue.async {
147-
guard let response = response as? HTTPURLResponse else {
148-
var apiError: APIClientError
149-
if let error = error {
150-
apiError = APIClientError.networkError(error)
151-
} else {
152-
apiError = APIClientError.networkError(URLRequestError.responseInvalid)
153-
}
154-
let result: APIResult<T> = .failure(apiError)
155-
requestBehaviour.onFailure(urlRequest: urlRequest, response: HTTPURLResponse(), error: apiError)
168+
let task = performRequest(request: request,
169+
urlRequest: urlRequest,
170+
cancellableRequest: cancellableRequest,
171+
requestBehaviour: requestBehaviour,
172+
currentUnauthorizedRetryCount: currentUnauthorizedRetryCount,
173+
currentRetryCount: currentRetryCount,
174+
completionQueue: completionQueue,
175+
complete: complete)
156176

157-
let response = {{ options.name }}Response<T>(request: request, result: result, urlRequest: urlRequest)
158-
requestBehaviour.onResponse(response: response.asAny())
177+
self.decodingQueue.async {
178+
task.resume()
179+
}
159180

160-
completionQueue.async {
161-
complete(response)
162-
}
181+
cancellableRequest.sessionTask = task
182+
}
183+
}
163184

164-
return
185+
private func performRequest<T>(request: {{ options.name }}Request<T>,
186+
urlRequest: URLRequest,
187+
cancellableRequest: Cancellable{{ options.name }}Request,
188+
requestBehaviour: {{ options.name }}RequestBehaviourGroup,
189+
currentUnauthorizedRetryCount: Int,
190+
currentRetryCount: Int,
191+
completionQueue: DispatchQueue,
192+
complete: @escaping ({{ options.name }}Response<T>) -> Void) -> URLSessionDataTask {
193+
let maxRetryCount = maxRetryCount
194+
return session.dataTask(with: urlRequest, completionHandler: { [weak self] data, response, error -> Void in
195+
// Handle response
196+
self?.decodingQueue.async {
197+
let newRetryCount = currentRetryCount + 1
198+
if API.shouldRetryRequest(currentRetryCount: newRetryCount,
199+
maxRetryCount: maxRetryCount,
200+
response: response) {
201+
let requestDelay = API.nextExponentialBackoffRequestDelay(currentRetryCount: newRetryCount)
202+
self?.decodingQueue.asyncAfter(deadline: .now() + .seconds(requestDelay)) { [weak self] in
203+
self?.makeNetworkRequest(request: request,
204+
urlRequest: urlRequest,
205+
cancellableRequest: cancellableRequest,
206+
requestBehaviour: requestBehaviour,
207+
currentUnauthorizedRetryCount: currentUnauthorizedRetryCount,
208+
currentRetryCount: newRetryCount,
209+
completionQueue: completionQueue,
210+
complete: complete)
165211
}
166-
212+
} else if let response = response as? HTTPURLResponse {
167213
self?.handleResponse(request: request,
168214
requestBehaviour: requestBehaviour,
169215
data: data,
170216
response: response,
171217
error: error,
172218
urlRequest: urlRequest,
173-
currentNumberOfRetries: currentNumberOfRetries,
219+
currentUnauthorizedRetryCount: currentUnauthorizedRetryCount,
220+
currentRetryCount: currentRetryCount,
174221
completionQueue: completionQueue,
175222
complete: complete)
176-
}
177-
})
223+
} else {
224+
var apiError: APIClientError
225+
if let error = error {
226+
apiError = APIClientError.networkError(error)
227+
} else {
228+
apiError = APIClientError.networkError(URLRequestError.responseInvalid)
229+
}
230+
let result: APIResult<T> = .failure(apiError)
231+
requestBehaviour.onFailure(urlRequest: urlRequest, response: HTTPURLResponse(), error: apiError)
178232

179-
self.decodingQueue.async {
180-
task.resume()
181-
}
233+
let response = {{ options.name }}Response<T>(request: request, result: result, urlRequest: urlRequest)
234+
requestBehaviour.onResponse(response: response.asAny())
182235

183-
cancellableRequest.sessionTask = task
184-
}
236+
completionQueue.async {
237+
complete(response)
238+
}
239+
}
240+
}
241+
})
185242
}
186243

187244
private func handleResponse<T>(request: {{ options.name }}Request<T>,
@@ -190,7 +247,8 @@ public class {{ options.name }}Client {
190247
response: HTTPURLResponse,
191248
error: Error?,
192249
urlRequest: URLRequest,
193-
currentNumberOfRetries: Int,
250+
currentUnauthorizedRetryCount: Int,
251+
currentRetryCount: Int,
194252
completionQueue: DispatchQueue,
195253
complete: @escaping ({{ options.name }}Response<T>) -> Void) {
196254
let result: APIResult<T>
@@ -209,11 +267,16 @@ public class {{ options.name }}Client {
209267
}
210268

211269
if response.statusCode == HttpStatusCode.unauthorized.rawValue
212-
&& currentNumberOfRetries < maxUnauthorizedRetryCount
270+
&& currentUnauthorizedRetryCount < maxUnauthorizedRetryCount
213271
&& IDKit.isSessionAvailable {
214272
IDKit.apiInducedRefresh { [weak self] error in
215273
guard let error = error else {
216-
self?.makeRequest(request, behaviours: requestBehaviour.behaviours, currentNumberOfRetries: currentNumberOfRetries + 1, completionQueue: completionQueue, complete: complete)
274+
self?.makeRequest(request,
275+
behaviours: requestBehaviour.behaviours,
276+
currentUnauthorizedRetryCount: currentUnauthorizedRetryCount + 1,
277+
currentRetryCount: currentRetryCount,
278+
completionQueue: completionQueue,
279+
complete: complete)
217280
return
218281
}
219282

0 commit comments

Comments
 (0)