Skip to content

Commit a637cf3

Browse files
authored
Add connection backoff (#1860)
Motivation: Connection attempts should be made with a backoff period between them. Modifications: - Add a connection backoff struct which can make an iterator to produces duration to backoff by Result: Can do backoff
1 parent dac2d68 commit a637cf3

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2024, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
18+
struct ConnectionBackoff {
19+
var initial: Duration
20+
var max: Duration
21+
var multiplier: Double
22+
var jitter: Double
23+
24+
init(initial: Duration, max: Duration, multiplier: Double, jitter: Double) {
25+
self.initial = initial
26+
self.max = max
27+
self.multiplier = multiplier
28+
self.jitter = jitter
29+
}
30+
31+
func makeIterator() -> Iterator {
32+
return Iterator(self)
33+
}
34+
35+
// Deliberately not conforming to `IteratorProtocol` as `next()` never returns `nil` which
36+
// isn't expressible via `IteratorProtocol`.
37+
struct Iterator {
38+
private var isInitial: Bool
39+
private var currentBackoffSeconds: Double
40+
41+
private let jitter: Double
42+
private let multiplier: Double
43+
private let maxBackoffSeconds: Double
44+
45+
init(_ backoff: ConnectionBackoff) {
46+
self.isInitial = true
47+
self.currentBackoffSeconds = Self.seconds(from: backoff.initial)
48+
self.jitter = backoff.jitter
49+
self.multiplier = backoff.multiplier
50+
self.maxBackoffSeconds = Self.seconds(from: backoff.max)
51+
}
52+
53+
private static func seconds(from duration: Duration) -> Double {
54+
var seconds = Double(duration.components.seconds)
55+
seconds += Double(duration.components.attoseconds) / 1e18
56+
return seconds
57+
}
58+
59+
private static func duration(from seconds: Double) -> Duration {
60+
let nanoseconds = seconds * 1e9
61+
let wholeNanos = Int64(nanoseconds)
62+
return .nanoseconds(wholeNanos)
63+
}
64+
65+
mutating func next() -> Duration {
66+
// The initial backoff doesn't get jittered.
67+
if self.isInitial {
68+
self.isInitial = false
69+
return Self.duration(from: self.currentBackoffSeconds)
70+
}
71+
72+
// Scale up the last backoff.
73+
self.currentBackoffSeconds *= self.multiplier
74+
75+
// Limit it to the max backoff.
76+
if self.currentBackoffSeconds > self.maxBackoffSeconds {
77+
self.currentBackoffSeconds = self.maxBackoffSeconds
78+
}
79+
80+
let backoff = self.currentBackoffSeconds
81+
let jitter = Double.random(in: -(self.jitter * backoff) ... self.jitter * backoff)
82+
let jitteredBackoff = backoff + jitter
83+
84+
return Self.duration(from: jitteredBackoff)
85+
}
86+
}
87+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2024, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import XCTest
18+
19+
@testable import GRPCHTTP2Core
20+
21+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
22+
final class ConnectionBackoffTests: XCTestCase {
23+
func testUnjitteredBackoff() {
24+
let backoff = ConnectionBackoff(
25+
initial: .seconds(10),
26+
max: .seconds(30),
27+
multiplier: 1.5,
28+
jitter: 0.0
29+
)
30+
31+
var iterator = backoff.makeIterator()
32+
XCTAssertEqual(iterator.next(), .seconds(10))
33+
// 10 * 1.5 = 15 seconds
34+
XCTAssertEqual(iterator.next(), .seconds(15))
35+
// 15 * 1.5 = 22.5 seconds
36+
XCTAssertEqual(iterator.next(), .seconds(22.5))
37+
// 22.5 * 1.5 = 33.75 seconds, clamped to 30 seconds, all future values will be the same.
38+
XCTAssertEqual(iterator.next(), .seconds(30))
39+
XCTAssertEqual(iterator.next(), .seconds(30))
40+
XCTAssertEqual(iterator.next(), .seconds(30))
41+
}
42+
43+
func testJitteredBackoff() {
44+
let backoff = ConnectionBackoff(
45+
initial: .seconds(10),
46+
max: .seconds(30),
47+
multiplier: 1.5,
48+
jitter: 0.1
49+
)
50+
51+
var iterator = backoff.makeIterator()
52+
53+
// Initial isn't jittered.
54+
XCTAssertEqual(iterator.next(), .seconds(10))
55+
56+
// Next value should be 10 * 1.5 = 15 seconds ± 1.5 seconds
57+
var expected: ClosedRange<Duration> = .seconds(13.5) ... .seconds(16.5)
58+
XCTAssert(expected.contains(iterator.next()))
59+
60+
// Next value should be 15 * 1.5 = 22.5 seconds ± 2.25 seconds
61+
expected = .seconds(20.25) ... .seconds(24.75)
62+
XCTAssert(expected.contains(iterator.next()))
63+
64+
// Next value should be 22.5 * 1.5 = 33.75 seconds, clamped to 30 seconds ± 3 seconds.
65+
// All future values will be in the same range.
66+
expected = .seconds(27) ... .seconds(33)
67+
XCTAssert(expected.contains(iterator.next()))
68+
XCTAssert(expected.contains(iterator.next()))
69+
XCTAssert(expected.contains(iterator.next()))
70+
}
71+
}

0 commit comments

Comments
 (0)