summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/Operations/RetryOperation.swift
blob: 1e73e81d792104ba62c5ce32c35eca6c0c260084 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//
//  RetryOperation.swift
//  MullvadVPN
//
//  Created by pronebird on 06/07/2020.
//  Copyright © 2020 Mullvad VPN AB. All rights reserved.
//

import Foundation

enum WaitStrategy {
    case immediate
    case constant(TimeInterval)

    var iterator: AnyIterator<TimeInterval> {
        switch self {
        case .immediate:
            return AnyIterator { .zero }
        case .constant(let constant):
            return AnyIterator { constant }
        }
    }
}

struct RetryStrategy {
    var maxRetries: Int
    var waitStrategy: WaitStrategy
    var waitTimerType: DelayTimerType
}

class RetryOperation<OperationType, Success, Failure: Error>: AsyncOperation, OutputOperation
    where OperationType: OutputOperation, OperationType.Output == Result<Success, Failure>
{
    typealias Output = OperationType.Output

    private let operationQueue = OperationQueue()

    private let producer: () -> OperationType
    private let delayIterator: AnyIterator<TimeInterval>

    private var retryCount: Int = 0
    private let retryStrategy: RetryStrategy

    private var childConfigurator: ((OperationType) -> Void)?

    init(underlyingQueue: DispatchQueue? = nil, strategy: RetryStrategy, producer: @escaping () -> OperationType) {
        operationQueue.underlyingQueue = underlyingQueue
        delayIterator = strategy.waitStrategy.iterator
        retryStrategy = strategy
        self.producer = producer
    }

    override func main() {
        retry()
    }

    override func operationDidCancel() {
        operationQueue.cancelAllOperations()
    }

    private func retry(delay: TimeInterval? = nil) {
        let child = producer()

        child.addDidFinishBlockObserver { [weak self] (operation) in
            guard let self = self else { return }

            // Operation finished without output set?
            guard let result = operation.output else {
                self.finish()
                return
            }

            self.synchronized {
                guard case .failure(let error) = result,
                    let delay = self.delayIterator.next(),
                    self.shouldRetry(error: error) else {
                    self.finish(with: result)
                    return
                }

                self.retryCount += 1
                self.retry(delay: delay)
            }
        }

        synchronized {
            childConfigurator?(child)
        }

        if let delay = delay {
            let delayOperation = DelayOperation(delay: delay, timerType: retryStrategy.waitTimerType)

            child.addDependency(delayOperation)
            operationQueue.addOperation(delayOperation)
        }

        operationQueue.addOperation(child)
    }

    private func setChildConfigurator(_ body: @escaping (OperationType) -> Void) {
        synchronized {
            self.childConfigurator = body
        }
    }

    private func shouldRetry(error: Failure) -> Bool {
        return retryCount < retryStrategy.maxRetries && !self.isCancelled
    }

}

extension RetryOperation: InputOperation where OperationType: InputOperation {
    typealias Input = OperationType.Input

    func operationDidSetInput(_ input: OperationType.Input?) {
        setChildConfigurator { (child) in
            child.input = input
        }
    }
}