summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-03-07 15:16:59 +0100
committerAndrej Mihajlov <and@mullvad.net>2023-03-22 16:42:30 +0100
commita51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb (patch)
tree85935823680100affad563ebeca45a07a71938ee /ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift
parent1c2c6f58dc1d175d00bea8037ca989ca80b1fcb8 (diff)
downloadmullvadvpn-a51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb.tar.xz
mullvadvpn-a51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb.zip
Add coordinators and app router
Fixes IOS-10
Diffstat (limited to 'ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift')
-rw-r--r--ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift191
1 files changed, 191 insertions, 0 deletions
diff --git a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift
new file mode 100644
index 0000000000..8c3100f92b
--- /dev/null
+++ b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift
@@ -0,0 +1,191 @@
+//
+// AddressCacheTracker.swift
+// MullvadVPN
+//
+// Created by pronebird on 08/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadLogging
+import MullvadREST
+import MullvadTypes
+import Operations
+import UIKit
+
+final class AddressCacheTracker {
+ /// Update interval (in seconds).
+ private static let updateInterval: TimeInterval = 60 * 60 * 24
+
+ /// Retry interval (in seconds).
+ private static let retryInterval: TimeInterval = 60 * 15
+
+ /// Logger.
+ private let logger = Logger(label: "AddressCache.Tracker")
+ private let application: UIApplication
+
+ /// REST API proxy.
+ private let apiProxy: REST.APIProxy
+
+ /// Address cache.
+ private let store: REST.AddressCache
+
+ /// A flag that indicates whether periodic updates are running
+ private var isPeriodicUpdatesEnabled = false
+
+ /// The date of last failed attempt.
+ private var lastFailureAttemptDate: Date?
+
+ /// Timer used for scheduling periodic updates.
+ private var timer: DispatchSourceTimer?
+
+ /// Operation queue.
+ private let operationQueue = AsyncOperationQueue.makeSerial()
+
+ /// Lock used for synchronizing member access.
+ private let nslock = NSLock()
+
+ /// Designated initializer
+ init(application: UIApplication, apiProxy: REST.APIProxy, store: REST.AddressCache) {
+ self.application = application
+ self.apiProxy = apiProxy
+ self.store = store
+ }
+
+ func startPeriodicUpdates() {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ guard !isPeriodicUpdatesEnabled else {
+ return
+ }
+
+ logger.debug("Start periodic address cache updates.")
+
+ isPeriodicUpdatesEnabled = true
+
+ let scheduleDate = _nextScheduleDate()
+
+ logger.debug("Schedule address cache update at \(scheduleDate.logFormatDate()).")
+
+ scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow)
+ }
+
+ func stopPeriodicUpdates() {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ guard isPeriodicUpdatesEnabled else { return }
+
+ logger.debug("Stop periodic address cache updates.")
+
+ isPeriodicUpdatesEnabled = false
+
+ timer?.cancel()
+ timer = nil
+ }
+
+ func updateEndpoints(completionHandler: ((Result<Bool, Error>) -> Void)? = nil) -> Cancellable {
+ let operation = ResultBlockOperation<Bool> { operation in
+ guard self.nextScheduleDate() <= Date() else {
+ operation.finish(result: .success(false))
+ return
+ }
+
+ let task = self.apiProxy.getAddressList(retryStrategy: .default) { result in
+ self.setEndpoints(from: result)
+
+ operation.finish(result: result.map { _ in true })
+ }
+
+ operation.addCancellationBlock {
+ task.cancel()
+ }
+ }
+
+ operation.completionQueue = .main
+ operation.completionHandler = completionHandler
+
+ operation.addObserver(
+ BackgroundObserver(
+ application: application,
+ name: "Update endpoints",
+ cancelUponExpiration: true
+ )
+ )
+
+ operationQueue.addOperation(operation)
+
+ return operation
+ }
+
+ func nextScheduleDate() -> Date {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _nextScheduleDate()
+ }
+
+ private func setEndpoints(from result: Result<[AnyIPEndpoint], Error>) {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ switch result {
+ case let .success(endpoints):
+ store.setEndpoints(endpoints)
+ lastFailureAttemptDate = nil
+
+ case let .failure(error as REST.Error):
+ logger.error(
+ error: error,
+ message: "Failed to update address cache."
+ )
+ fallthrough
+
+ default:
+ lastFailureAttemptDate = Date()
+ }
+ }
+
+ private func scheduleEndpointsUpdate(startTime: DispatchWallTime) {
+ let newTimer = DispatchSource.makeTimerSource()
+ newTimer.setEventHandler { [weak self] in
+ self?.handleTimer()
+ }
+
+ newTimer.schedule(wallDeadline: startTime)
+ newTimer.activate()
+
+ timer?.cancel()
+ timer = newTimer
+ }
+
+ private func handleTimer() {
+ _ = updateEndpoints { _ in
+ self.nslock.lock()
+ defer { self.nslock.unlock() }
+
+ guard self.isPeriodicUpdatesEnabled else { return }
+
+ let scheduleDate = self._nextScheduleDate()
+
+ self.logger
+ .debug("Schedule next address cache update at \(scheduleDate.logFormatDate()).")
+
+ self.scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow)
+ }
+ }
+
+ private func _nextScheduleDate() -> Date {
+ let nextDate = lastFailureAttemptDate.map { date in
+ return Date(
+ timeInterval: Self.retryInterval,
+ since: date
+ )
+ } ?? Date(
+ timeInterval: Self.updateInterval,
+ since: store.getLastUpdateDate()
+ )
+
+ return max(nextDate, Date())
+ }
+}