Skip to content

Commit

Permalink
feat(auto-retry): fixes for AutomaticRetry
Browse files Browse the repository at this point in the history
  • Loading branch information
jguz-pubnub committed Dec 19, 2023
1 parent 95aac0a commit 41eccfb
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 150 deletions.
81 changes: 47 additions & 34 deletions Sources/PubNub/Networking/Request/Operators/AutomaticRetry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ public struct AutomaticRetry: RequestOperator, Hashable {
policy: .defaultExponential,
retryableURLErrorCodes: [.notConnectedToInternet]
)

// The minimum value allowed between retries
static let minDelay: UInt = 2

/// Provides the action taken when a retry is to be performed
public enum ReconnectionPolicy: Hashable {
/// Exponential backoff with base/scale factor of 2, and a 300s max delay
public static let defaultExponential: ReconnectionPolicy = .exponential(base: 2, scale: 2, maxDelay: 300)
/// Linear reconnect every 3 seconds
public static let defaultLinear: ReconnectionPolicy = .linear(delay: 3)
/// Exponential backoff with base/scale factor of 2, and a 150s max delay
public static let defaultExponential: ReconnectionPolicy = .exponential(minDelay: minDelay, maxDelay: 150)
/// Linear reconnect every 2 seconds
public static let defaultLinear: ReconnectionPolicy = .linear(delay: Double(minDelay))

/// Reconnect with an exponential backoff
case exponential(base: UInt, scale: Double, maxDelay: UInt)
case exponential(minDelay: UInt, maxDelay: UInt)
/// Attempt to reconnect every X seconds
case linear(delay: Double)

Expand All @@ -45,15 +47,15 @@ public struct AutomaticRetry: RequestOperator, Hashable {
let randomDelay = Double.random(in: 0...1)

switch self {
case let .exponential(base, scale, maxDelay):
return exponentialBackoffDelay(for: base, scale: scale, maxDelay: maxDelay, current: retryAttempt) + randomDelay
case let .exponential(minDelay, maxDelay):
return exponentialBackoffDelay(minDelay: minDelay, maxDelay: maxDelay, current: retryAttempt) + randomDelay
case let .linear(delay):
return delay + randomDelay
}
}

func exponentialBackoffDelay(for base: UInt, scale: Double, maxDelay: UInt, current retryCount: Int) -> Double {
return min(pow(Double(base), Double(retryCount)) * scale, Double(maxDelay))
func exponentialBackoffDelay(minDelay: UInt, maxDelay: UInt, current retryCount: Int) -> Double {
return min(Double(maxDelay), Double(minDelay) * pow(2, Double(retryCount)))
}
}

Expand Down Expand Up @@ -87,9 +89,9 @@ public struct AutomaticRetry: RequestOperator, Hashable {
public let excluded: [AutomaticRetry.Endpoint]

public init(
retryLimit: UInt = 2,
retryLimit: UInt = 6,
policy: ReconnectionPolicy = .defaultExponential,
retryableHTTPStatusCodes: Set<Int> = [500],
retryableHTTPStatusCodes: Set<Int> = [500, 429],
retryableURLErrorCodes: Set<URLError.Code> = AutomaticRetry.defaultRetryableURLErrorCodes,
excluded endpoints: [AutomaticRetry.Endpoint] = [
.addChannelsToGroup,
Expand Down Expand Up @@ -131,31 +133,42 @@ public struct AutomaticRetry: RequestOperator, Hashable {
]
) {
switch policy {
case let .exponential(base, scale, max):
switch (true, true) {
case (base < 2, scale < 0):
PubNub.log.warn("The `exponential.base` must be a minimum of 2.")
PubNub.log.warn("The `exponential.scale` must be a positive value.")
self.policy = .exponential(base: 2, scale: 0, maxDelay: max)
case (base < 2, scale >= 0):
PubNub.log.warn("The `exponential.base` must be a minimum of 2.")
self.policy = .exponential(base: 2, scale: scale, maxDelay: max)
case (base >= 2, scale < 0):
PubNub.log.warn("The `exponential.scale` must be a positive value.")
self.policy = .exponential(base: base, scale: 0, maxDelay: max)
default:
self.policy = policy
case let .exponential(minDelay, maxDelay):
var finalMinDelay: UInt = minDelay
var finalMaxDelay: UInt = maxDelay
var finalRetryLimit: UInt = retryLimit

if finalRetryLimit > 10 {
PubNub.log.warn("The `retryLimit` for exponential policy must be less than or equal 10")
finalRetryLimit = 10
}
if finalMinDelay < Self.minDelay {
PubNub.log.warn("The `minDelay` must be a minimum of \(Self.minDelay)")
finalMinDelay = Self.minDelay
}
if finalMinDelay > finalMaxDelay {
PubNub.log.warn("The `minDelay` \"\(minDelay)\" must be greater or equal `maxDelay` \"\(maxDelay)\"")
finalMaxDelay = minDelay
}
self.retryLimit = finalRetryLimit
self.policy = .exponential(minDelay: finalMinDelay, maxDelay: finalMaxDelay)

case let .linear(delay):
if delay < 0 {
PubNub.log.warn("The `linear.delay` must be a positive value.")
self.policy = .linear(delay: 0)
} else {
self.policy = policy
var finalRetryLimit = retryLimit
var finalDelay = delay

if finalRetryLimit > 10 {
PubNub.log.warn("The `retryLimit` for linear policy must be less than or equal 10")
finalRetryLimit = 10
}
if finalDelay < 0 || UInt(finalDelay) < Self.minDelay {
PubNub.log.warn("The `linear.delay` must be greater than or equal \(Self.minDelay).")
finalDelay = Double(Self.minDelay)
}
self.retryLimit = finalRetryLimit
self.policy = .linear(delay: finalDelay)
}

self.retryLimit = retryLimit

self.retryableHTTPStatusCodes = retryableHTTPStatusCodes
self.retryableURLErrorCodes = retryableURLErrorCodes
self.excluded = endpoints
Expand Down Expand Up @@ -184,7 +197,7 @@ public struct AutomaticRetry: RequestOperator, Hashable {

func shouldRetry(response: HTTPURLResponse?, error: Error) -> Bool {
if let statusCode = response?.statusCode {
return retryableHTTPStatusCodes.contains(statusCode) || statusCode == 429
return retryableHTTPStatusCodes.contains(statusCode)
} else if let errorCode = error.urlError?.code, retryableURLErrorCodes.contains(errorCode) {
return true
} else if let errorCode = error.pubNubError?.underlying?.urlError?.code, retryableURLErrorCodes.contains(errorCode) {
Expand Down
Loading

0 comments on commit 41eccfb

Please sign in to comment.