diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-06-20 12:22:05 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-06-20 12:22:05 +0200 |
| commit | 19bb6e6220d79a899409755d187ad641679f327d (patch) | |
| tree | 6bc525a14295775fb6a5b9e11a6894d72e30f687 /ios/MullvadVPN/AddressCache | |
| parent | d88d90c5a937ffbf42111e8721f6829e1040efdf (diff) | |
| parent | f456963c67db335bb448d901065e453e0408deb8 (diff) | |
| download | mullvadvpn-19bb6e6220d79a899409755d187ad641679f327d.tar.xz mullvadvpn-19bb6e6220d79a899409755d187ad641679f327d.zip | |
Merge branch 'background-task-appdelegate'
Diffstat (limited to 'ios/MullvadVPN/AddressCache')
| -rw-r--r-- | ios/MullvadVPN/AddressCache/AddressCacheTracker.swift | 227 | ||||
| -rw-r--r-- | ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift | 86 |
2 files changed, 108 insertions, 205 deletions
diff --git a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift index bbfaeb08c8..70397032ca 100644 --- a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift +++ b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift @@ -7,11 +7,19 @@ // import UIKit -import BackgroundTasks import Logging extension AddressCache { + class Tracker { + /// Shared instance. + static let shared: AddressCache.Tracker = { + return AddressCache.Tracker( + apiProxy: REST.ProxyFactory.shared.createAPIProxy(), + store: AddressCache.Store.shared + ) + }() + /// Update interval (in seconds). private static let updateInterval: TimeInterval = 60 * 60 * 24 @@ -43,58 +51,71 @@ extension AddressCache { return operationQueue }() - /// Queue used for synchronizing access to instance members. - private let stateQueue = DispatchQueue(label: "AddressCache.Tracker.stateQueue") + /// Lock used for synchronizing member access. + private let nslock = NSLock() /// Designated initializer - init(apiProxy: REST.APIProxy, store: AddressCache.Store) { + private init(apiProxy: REST.APIProxy, store: AddressCache.Store) { self.apiProxy = apiProxy self.store = store } func startPeriodicUpdates() { - stateQueue.async { - guard !self.isPeriodicUpdatesEnabled else { - return - } + nslock.lock() + defer { nslock.unlock() } - self.logger.debug("Start periodic address cache updates") + guard !isPeriodicUpdatesEnabled else { + return + } - self.isPeriodicUpdatesEnabled = true + logger.debug("Start periodic address cache updates.") - let scheduleDate = self.nextScheduleDate() + isPeriodicUpdatesEnabled = true - self.logger.debug("Schedule address cache update on \(scheduleDate.logFormatDate())") + let scheduleDate = _nextScheduleDate() - self.scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow) - } + logger.debug("Schedule address cache update at \(scheduleDate.logFormatDate()).") + + scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow) } func stopPeriodicUpdates() { - stateQueue.async { - guard self.isPeriodicUpdatesEnabled else { return } + nslock.lock() + defer { nslock.unlock() } - self.logger.debug("Stop periodic address cache updates") + guard isPeriodicUpdatesEnabled else { return } - self.isPeriodicUpdatesEnabled = false + logger.debug("Stop periodic address cache updates.") - self.timer?.cancel() - self.timer = nil - } + isPeriodicUpdatesEnabled = false + + timer?.cancel() + timer = nil } - func updateEndpoints(completionHandler: ((_ completion: OperationCompletion<CacheUpdateResult, Error>) -> Void)? = nil) -> Cancellable { - let operation = UpdateAddressCacheOperation( - dispatchQueue: stateQueue, - apiProxy: apiProxy, - store: store, - updateInterval: Self.updateInterval, - completionHandler: { [weak self] completion in - self?.handleCacheUpdateCompletion(completion) + func updateEndpoints( + completionHandler: ((OperationCompletion<Bool, Error>) -> Void)? = nil + ) -> Cancellable + { + let operation = ResultBlockOperation<Bool, Error> { operation in + guard self.nextScheduleDate() <= Date() else { + operation.finish(completion: .success(false)) + return + } - completionHandler?(completion) + let task = self.apiProxy.getAddressList(retryStrategy: .default) { completion in + operation.finish( + completion: self.handleResponse(completion: completion) + ) } - ) + + operation.addCancellationBlock { + task.cancel() + } + } + + operation.completionQueue = .main + operation.completionHandler = completionHandler operation.addObserver( BackgroundObserver(name: "Update endpoints", cancelUponExpiration: true) @@ -105,6 +126,45 @@ extension AddressCache { return operation } + func nextScheduleDate() -> Date { + nslock.lock() + defer { nslock.unlock() } + + return _nextScheduleDate() + } + + private func handleResponse( + completion: OperationCompletion<[AnyIPEndpoint], REST.Error> + ) -> OperationCompletion<Bool, Error> + { + let mappedCompletion = completion + .flatMapError { error -> OperationCompletion<[AnyIPEndpoint], REST.Error> in + if case URLError.cancelled = error { + return .cancelled + } else { + return .failure(error) + } + } + .tryMap { endpoints -> Bool in + try store.setEndpoints(endpoints) + + return true + } + + nslock.lock() + lastFailureAttemptDate = mappedCompletion.isSuccess ? nil : Date() + nslock.unlock() + + if let error = mappedCompletion.error { + logger.error( + chainedError: AnyChainedError(error), + message: "Failed to update address cache." + ) + } + + return mappedCompletion + } + private func scheduleEndpointsUpdate(startTime: DispatchWallTime) { let newTimer = DispatchSource.makeTimerSource() newTimer.setEventHandler { [weak self] in @@ -119,104 +179,33 @@ extension AddressCache { } private func handleTimer() { - _ = updateEndpoints { result in + _ = updateEndpoints { _ in + self.nslock.lock() + defer { self.nslock.unlock() } + guard self.isPeriodicUpdatesEnabled else { return } - let scheduleDate = self.nextScheduleDate() + let scheduleDate = self._nextScheduleDate() - self.logger.debug("Schedule next address cache update on \(scheduleDate.logFormatDate())") + self.logger.debug("Schedule next address cache update at \(scheduleDate.logFormatDate()).") self.scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow) } } - private func nextScheduleDate() -> Date { - if let lastFailureAttemptDate = lastFailureAttemptDate { - return Date(timeInterval: Self.retryInterval, since: lastFailureAttemptDate) - } else { - let updatedAt = store.getLastUpdateDate() - - return Date(timeInterval: Self.updateInterval, since: updatedAt) - } - } - - private func handleCacheUpdateCompletion(_ completion: OperationCompletion<AddressCache.CacheUpdateResult, Error>) { - switch completion { - case .success(let updateResult): - switch updateResult { - case .finished: - logger.debug("Finished updating address cache.") - case .throttled: - logger.debug("Address cache update was throttled.") - } - - lastFailureAttemptDate = nil - - case .failure(let error): - logger.error(chainedError: AnyChainedError(error), message: "Failed to update address cache.") - lastFailureAttemptDate = Date() - - case .cancelled: - logger.debug("Address cache update was cancelled.") - lastFailureAttemptDate = Date() - } - } - - } -} - -// MARK: - Background tasks - -@available(iOS 13.0, *) -extension AddressCache.Tracker { - - /// Register background task with scheduler. - func registerBackgroundTask() { - let taskIdentifier = ApplicationConfiguration.addressCacheUpdateTaskIdentifier - - let isRegistered = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in - self.handleBackgroundTask(task as! BGProcessingTask) - } - - if isRegistered { - logger.debug("Registered address cache update task") - } else { - logger.error("Failed to register address cache update task") - } - } - - /// Create and submit task request to scheduler. - func scheduleBackgroundTask() throws { - let beginDate = nextScheduleDate() - - logger.debug("Schedule address cache update task on \(beginDate.logFormatDate())") - - let taskIdentifier = ApplicationConfiguration.addressCacheUpdateTaskIdentifier - - let request = BGProcessingTaskRequest(identifier: taskIdentifier) - request.earliestBeginDate = beginDate - request.requiresNetworkConnectivity = true - - return try BGTaskScheduler.shared.submit(request) - } - - /// Background task handler. - private func handleBackgroundTask(_ task: BGProcessingTask) { - logger.debug("Start address cache update task") - - let cancellable = updateEndpoints { completion in - do { - // Schedule next background task - try self.scheduleBackgroundTask() - } catch { - self.logger.error(chainedError: AnyChainedError(error), message: "Failed to schedule next address cache update task") - } + private func _nextScheduleDate() -> Date { + let nextDate = lastFailureAttemptDate.map { date in + return Date( + timeInterval: Self.retryInterval, + since: date + ) + } ?? Date( + timeInterval: Self.updateInterval, + since: store.getLastUpdateDate() + ) - task.setTaskCompleted(success: completion.isSuccess) + return max(nextDate, Date()) } - task.expirationHandler = { - cancellable.cancel() - } } } diff --git a/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift b/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift deleted file mode 100644 index 4eaeda18e1..0000000000 --- a/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// UpdateAddressCacheOperation.swift -// MullvadVPN -// -// Created by pronebird on 08/12/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -extension AddressCache { - - enum CacheUpdateResult { - /// Address cache update was throttled as it was requested too early. - case throttled(_ lastUpdateDate: Date) - - /// Address cache is successfully updated. - case finished - } - - class UpdateAddressCacheOperation: ResultOperation<CacheUpdateResult, Error> { - private let apiProxy: REST.APIProxy - private let store: AddressCache.Store - private let updateInterval: TimeInterval - - private var requestTask: Cancellable? - - init( - dispatchQueue: DispatchQueue, - apiProxy: REST.APIProxy, - store: AddressCache.Store, - updateInterval: TimeInterval, - completionHandler: CompletionHandler? - ) - { - self.apiProxy = apiProxy - self.store = store - self.updateInterval = updateInterval - - super.init( - dispatchQueue: dispatchQueue, - completionQueue: dispatchQueue, - completionHandler: completionHandler - ) - } - - override func main() { - let lastUpdate = store.getLastUpdateDate() - let nextUpdate = Date(timeInterval: updateInterval, since: lastUpdate) - - guard nextUpdate <= Date() else { - finish(completion: .success(.throttled(lastUpdate))) - return - } - - requestTask = apiProxy.getAddressList(retryStrategy: .default) { [weak self] completion in - self?.dispatchQueue.async { - self?.handleResponse(completion) - } - } - } - - override func operationDidCancel() { - requestTask?.cancel() - requestTask = nil - } - - private func handleResponse(_ completion: OperationCompletion<[AnyIPEndpoint], REST.Error>) { - let mappedCompletion = completion - .flatMapError { error -> OperationCompletion<[AnyIPEndpoint], Error> in - if case URLError.cancelled = error { - return .cancelled - } else { - return .failure(error) - } - } - .tryMap { endpoints -> CacheUpdateResult in - try store.setEndpoints(endpoints) - - return .finished - } - - finish(completion: mappedCompletion) - } - } -} |
