summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/AddressCache
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-06-20 12:22:05 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-06-20 12:22:05 +0200
commit19bb6e6220d79a899409755d187ad641679f327d (patch)
tree6bc525a14295775fb6a5b9e11a6894d72e30f687 /ios/MullvadVPN/AddressCache
parentd88d90c5a937ffbf42111e8721f6829e1040efdf (diff)
parentf456963c67db335bb448d901065e453e0408deb8 (diff)
downloadmullvadvpn-19bb6e6220d79a899409755d187ad641679f327d.tar.xz
mullvadvpn-19bb6e6220d79a899409755d187ad641679f327d.zip
Merge branch 'background-task-appdelegate'
Diffstat (limited to 'ios/MullvadVPN/AddressCache')
-rw-r--r--ios/MullvadVPN/AddressCache/AddressCacheTracker.swift227
-rw-r--r--ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift86
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)
- }
- }
-}