Skip to content

Commit 0986e43

Browse files
committed
fix(ios): add reconnection jitter and max retry limit
Add jitter to exponential backoff in both BufferWebSocketClient and ReconnectionManager to prevent thundering herd on server restart. Cap BufferWebSocketClient reconnection at 10 attempts. Integrates PR amantus-ai#593.
1 parent c68a7b5 commit 0986e43

2 files changed

Lines changed: 46 additions & 7 deletions

File tree

ios/VibeTunnel/Services/BufferWebSocketClient.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -836,13 +836,30 @@ class BufferWebSocketClient: NSObject {
836836
self.scheduleReconnect()
837837
}
838838

839+
/// Maximum number of reconnection attempts before giving up
840+
private static let maxReconnectAttempts = 10
841+
839842
private func scheduleReconnect() {
840843
guard self.reconnectTask == nil else { return }
841844

842-
let delay = min(pow(2.0, Double(reconnectAttempts)), 30.0)
845+
// Stop reconnecting after max attempts to prevent infinite retries
846+
guard self.reconnectAttempts < Self.maxReconnectAttempts else {
847+
self.logger.warning(
848+
"Max reconnect attempts (\(Self.maxReconnectAttempts)) reached, stopping")
849+
self.connectionError = WebSocketError.connectionFailed
850+
return
851+
}
852+
853+
// Calculate exponential backoff with jitter to prevent thundering herd
854+
let baseDelay = pow(2.0, Double(self.reconnectAttempts))
855+
let cappedDelay = min(baseDelay, 30.0)
856+
let jitter = cappedDelay * 0.3 * Double.random(in: 0...1)
857+
let delay = cappedDelay + jitter
858+
843859
self.reconnectAttempts += 1
844860

845-
self.logger.info("Reconnecting in \(delay)s (attempt \(self.reconnectAttempts))")
861+
self.logger.info(
862+
"Reconnecting in \(String(format: "%.1f", delay))s (attempt \(self.reconnectAttempts)/\(Self.maxReconnectAttempts))")
846863

847864
self.reconnectTask = Task { @MainActor [weak self] in
848865
let nanoseconds = UInt64(delay * 1_000_000_000)

ios/VibeTunnel/Services/ReconnectionManager.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,12 @@ class ReconnectionManager {
8888
self.currentRetry += 1
8989

9090
if self.currentRetry < self.maxRetries {
91-
// Calculate exponential backoff
92-
let backoffSeconds = min(pow(2.0, Double(currentRetry - 1)), 60.0)
91+
// Calculate exponential backoff with jitter to prevent thundering herd
92+
let backoffSeconds = Self.calculateBackoff(
93+
attempt: self.currentRetry,
94+
baseDelay: 1.0,
95+
maxDelay: 60.0,
96+
jitterFactor: 0.3)
9397
self.nextRetryTime = Date().addingTimeInterval(backoffSeconds)
9498

9599
try? await Task.sleep(for: .seconds(backoffSeconds))
@@ -110,15 +114,33 @@ class ReconnectionManager {
110114
// MARK: - Exponential Backoff Calculator
111115

112116
extension ReconnectionManager {
113-
/// Calculate the next retry delay using exponential backoff
117+
/// Calculate the next retry delay using exponential backoff with jitter.
118+
///
119+
/// Jitter prevents the "thundering herd" problem where many clients
120+
/// reconnect simultaneously after a server restart or network outage.
121+
/// The randomized delay spreads reconnection attempts over time,
122+
/// reducing server load spikes.
123+
///
124+
/// - Parameters:
125+
/// - attempt: The current retry attempt number (1-indexed)
126+
/// - baseDelay: Initial delay in seconds (default: 1.0)
127+
/// - maxDelay: Maximum delay cap in seconds (default: 60.0)
128+
/// - jitterFactor: Randomization factor (0.0-1.0, default: 0.3)
129+
/// - Returns: Calculated delay with jitter applied
114130
static func calculateBackoff(
115131
attempt: Int,
116132
baseDelay: TimeInterval = 1.0,
117-
maxDelay: TimeInterval = 60.0)
133+
maxDelay: TimeInterval = 60.0,
134+
jitterFactor: Double = 0.3)
118135
-> TimeInterval
119136
{
120137
let exponentialDelay = baseDelay * pow(2.0, Double(attempt - 1))
121-
return min(exponentialDelay, maxDelay)
138+
let cappedDelay = min(exponentialDelay, maxDelay)
139+
140+
// Apply jitter: add random value between 0 and jitterFactor * delay
141+
// This spreads reconnection attempts to prevent thundering herd
142+
let jitter = cappedDelay * jitterFactor * Double.random(in: 0...1)
143+
return cappedDelay + jitter
122144
}
123145
}
124146

0 commit comments

Comments
 (0)