Skip to content

Commit 9cbc639

Browse files
committed
Add a Transformer iterator to apply a limiter to jittering
1 parent bd71954 commit 9cbc639

File tree

6 files changed

+91
-11
lines changed

6 files changed

+91
-11
lines changed

ios/MullvadREST/RetryStrategy/ExponentialBackoff.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import MullvadTypes
1212
struct ExponentialBackoff: IteratorProtocol {
1313
private var _next: Duration
1414
private let multiplier: UInt64
15-
private let maxDelay: Duration?
15+
private let maxDelay: Duration
1616

17-
init(initial: Duration, multiplier: UInt64, maxDelay: Duration? = nil) {
17+
init(initial: Duration, multiplier: UInt64, maxDelay: Duration) {
1818
_next = initial
1919
self.multiplier = multiplier
2020
self.maxDelay = maxDelay
@@ -23,7 +23,7 @@ struct ExponentialBackoff: IteratorProtocol {
2323
mutating func next() -> Duration? {
2424
let next = _next
2525

26-
if let maxDelay, next > maxDelay {
26+
if next > maxDelay {
2727
return maxDelay
2828
}
2929

ios/MullvadREST/RetryStrategy/Jittered.swift

+16
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,19 @@ struct Jittered<InnerIterator: IteratorProtocol>: IteratorProtocol
2727
return .milliseconds(millisWithJitter)
2828
}
2929
}
30+
31+
/// Iterator that applies a transform function to the result of another iterator.
32+
struct Transformer<Inner: IteratorProtocol>: IteratorProtocol {
33+
typealias Element = Inner.Element
34+
private var inner: Inner
35+
private let transformer: (Inner.Element?) -> Inner.Element?
36+
37+
init(inner: Inner, transform: @escaping (Inner.Element?) -> Inner.Element?) {
38+
self.inner = inner
39+
self.transformer = transform
40+
}
41+
42+
mutating func next() -> Inner.Element? {
43+
transformer(inner.next())
44+
}
45+
}

ios/MullvadREST/RetryStrategy/RetryStrategy.swift

+12-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,17 @@ extension REST {
2525
let inner = delay.makeIterator()
2626

2727
if applyJitter {
28-
return AnyIterator(Jittered(inner))
28+
return switch delay {
29+
case .never:
30+
AnyIterator(inner)
31+
case .constant:
32+
AnyIterator(Jittered(inner))
33+
case let .exponentialBackoff(_, _, maxDelay):
34+
AnyIterator(Transformer(inner: Jittered(inner)) { nextValue in
35+
guard let nextValue else { return maxDelay }
36+
return nextValue >= maxDelay ? maxDelay : nextValue
37+
})
38+
}
2939
} else {
3040
return AnyIterator(inner)
3141
}
@@ -68,7 +78,7 @@ extension REST {
6878
case constant(Duration)
6979

7080
/// Exponential backoff.
71-
case exponentialBackoff(initial: Duration, multiplier: UInt64, maxDelay: Duration?)
81+
case exponentialBackoff(initial: Duration, multiplier: UInt64, maxDelay: Duration)
7282

7383
func makeIterator() -> AnyIterator<Duration> {
7484
switch self {

ios/MullvadRESTTests/ExponentialBackoffTests.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ import XCTest
1212

1313
final class ExponentialBackoffTests: XCTestCase {
1414
func testExponentialBackoff() {
15-
var backoff = ExponentialBackoff(initial: .seconds(2), multiplier: 3)
15+
var backoff = ExponentialBackoff(initial: .seconds(2), multiplier: 3, maxDelay: .seconds(18))
1616

1717
XCTAssertEqual(backoff.next(), .seconds(2))
1818
XCTAssertEqual(backoff.next(), .seconds(6))
1919
XCTAssertEqual(backoff.next(), .seconds(18))
2020
}
2121

2222
func testAtMaximumValue() {
23-
var backoff = ExponentialBackoff(initial: .milliseconds(.max - 1), multiplier: 2)
23+
var backoff = ExponentialBackoff(initial: .milliseconds(.max - 1), multiplier: 2, maxDelay: .seconds(.max - 1))
2424

2525
XCTAssertEqual(backoff.next(), .milliseconds(.max - 1))
2626
XCTAssertEqual(backoff.next(), .milliseconds(.max))
@@ -40,20 +40,20 @@ final class ExponentialBackoffTests: XCTestCase {
4040
}
4141

4242
func testMinimumValue() {
43-
var backoff = ExponentialBackoff(initial: .milliseconds(0), multiplier: 10)
43+
var backoff = ExponentialBackoff(initial: .milliseconds(0), multiplier: 10, maxDelay: .milliseconds(0))
4444

4545
XCTAssertEqual(backoff.next(), .milliseconds(0))
4646
XCTAssertEqual(backoff.next(), .milliseconds(0))
4747

48-
backoff = ExponentialBackoff(initial: .milliseconds(1), multiplier: 0)
48+
backoff = ExponentialBackoff(initial: .milliseconds(1), multiplier: 0, maxDelay: .zero)
4949

50-
XCTAssertEqual(backoff.next(), .milliseconds(1))
50+
XCTAssertEqual(backoff.next(), .milliseconds(0))
5151
XCTAssertEqual(backoff.next(), .milliseconds(0))
5252
}
5353

5454
func testJitter() {
5555
let initial: Duration = .milliseconds(500)
56-
var iterator = Jittered(ExponentialBackoff(initial: initial, multiplier: 3))
56+
var iterator = Jittered(ExponentialBackoff(initial: initial, multiplier: 3, maxDelay: .milliseconds(1500)))
5757

5858
XCTAssertGreaterThanOrEqual(iterator.next()!, initial)
5959
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// RetryStrategyTests.swift
3+
// MullvadRESTTests
4+
//
5+
// Created by Marco Nikic on 2024-06-07.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
@testable import MullvadREST
11+
@testable import MullvadTypes
12+
import XCTest
13+
14+
class RetryStrategyTests: XCTestCase {
15+
func testJitteredBackoffDoesNotGoBeyondMaxDelay() throws {
16+
let retryDelay = REST.RetryDelay.exponentialBackoff(initial: .seconds(1), multiplier: 2, maxDelay: .seconds(10))
17+
let retry = REST.RetryStrategy(maxRetryCount: 0, delay: retryDelay, applyJitter: true)
18+
let iterator = retry.makeDelayIterator()
19+
var previousDelay = Duration(secondsComponent: 0, attosecondsComponent: 0)
20+
21+
for _ in 0 ... 10 {
22+
let currentDelay = try XCTUnwrap(iterator.next())
23+
XCTAssertLessThanOrEqual(previousDelay, currentDelay)
24+
previousDelay = currentDelay
25+
}
26+
}
27+
28+
func testJitteredConstantCannotBeMoreThanDouble() throws {
29+
let retryDelay = REST.RetryDelay.constant(.seconds(10))
30+
let retry = REST.RetryStrategy(maxRetryCount: 0, delay: retryDelay, applyJitter: true)
31+
let iterator = retry.makeDelayIterator()
32+
let minimumDelay = Duration(secondsComponent: 10, attosecondsComponent: 0)
33+
let maximumDelay = Duration(secondsComponent: 20, attosecondsComponent: 0)
34+
35+
for _ in 0 ... 10 {
36+
let currentDelay = try XCTUnwrap(iterator.next())
37+
let maximumJitterRange = minimumDelay ... maximumDelay
38+
print(currentDelay)
39+
XCTAssertLessThanOrEqual(maximumJitterRange.lowerBound, currentDelay)
40+
XCTAssertGreaterThanOrEqual(maximumJitterRange.upperBound, currentDelay)
41+
}
42+
}
43+
44+
func testCannotApplyJitterToNeverRetry() throws {
45+
let retryDelay = REST.RetryDelay.never
46+
let retry = REST.RetryStrategy(maxRetryCount: 0, delay: retryDelay, applyJitter: true)
47+
let iterator = retry.makeDelayIterator()
48+
XCTAssertNil(iterator.next())
49+
}
50+
}

ios/MullvadVPN.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@
686686
A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; };
687687
A91D78E32B03BDF200FCD5D3 /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; };
688688
A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; };
689+
A91EBEDA2C1337040004A84D /* RetryStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EBED92C1337040004A84D /* RetryStrategyTests.swift */; };
689690
A93181A12B727ED700E341D2 /* TunnelSettingsV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93181A02B727ED700E341D2 /* TunnelSettingsV4.swift */; };
690691
A932D9EF2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9EE2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift */; };
691692
A932D9F32B5EB61100999395 /* HeadRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9F22B5EB61100999395 /* HeadRequestTests.swift */; };
@@ -2025,6 +2026,7 @@
20252026
A91614D02B108D1B00F416EB /* TransportLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportLayer.swift; sourceTree = "<group>"; };
20262027
A91614D52B10B26B00F416EB /* TunnelControlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelControlViewModel.swift; sourceTree = "<group>"; };
20272028
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; };
2029+
A91EBED92C1337040004A84D /* RetryStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetryStrategyTests.swift; sourceTree = "<group>"; };
20282030
A92962582B1F4FDB00DFB93B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
20292031
A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettings.swift; sourceTree = "<group>"; };
20302032
A92ECC232A7802520052F1B1 /* StoredAccountData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredAccountData.swift; sourceTree = "<group>"; };
@@ -3745,6 +3747,7 @@
37453747
A932D9F22B5EB61100999395 /* HeadRequestTests.swift */,
37463748
58BDEB9E2A98F6B400F578F2 /* Mocks */,
37473749
58B4656F2A98C53300467203 /* RequestExecutorTests.swift */,
3750+
A91EBED92C1337040004A84D /* RetryStrategyTests.swift */,
37483751
F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */,
37493752
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */,
37503753
);
@@ -6028,6 +6031,7 @@
60286031
58BDEB9D2A98F69E00F578F2 /* MemoryCache.swift in Sources */,
60296032
58BDEB9B2A98F58600F578F2 /* TimeServerProxy.swift in Sources */,
60306033
A932D9F52B5EBB9D00999395 /* RESTTransportStub.swift in Sources */,
6034+
A91EBEDA2C1337040004A84D /* RetryStrategyTests.swift in Sources */,
60316035
58BDEB992A98F4ED00F578F2 /* AnyTransport.swift in Sources */,
60326036
A932D9F32B5EB61100999395 /* HeadRequestTests.swift in Sources */,
60336037
);

0 commit comments

Comments
 (0)