diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-03-07 15:16:59 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-03-22 16:42:30 +0100 |
| commit | a51757ce590b5063c1c8099b3ed8ea0fa8b3bcdb (patch) | |
| tree | 85935823680100affad563ebeca45a07a71938ee /ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift | |
| parent | 1c2c6f58dc1d175d00bea8037ca989ca80b1fcb8 (diff) | |
| download | mullvadvpn-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.swift | 191 |
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()) + } +} |
