summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/RelayCache
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-03-15 12:45:35 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-03-24 14:00:24 +0100
commit0cd501d8173087cc46593e6aaf00a15c0f3ca25f (patch)
tree8144b8f60d8165b7261a4ee109b0f5963b554328 /ios/MullvadVPN/RelayCache
parentc69ebd383d617efbba1b0afac98db783f0143b3d (diff)
downloadmullvadvpn-0cd501d8173087cc46593e6aaf00a15c0f3ca25f.tar.xz
mullvadvpn-0cd501d8173087cc46593e6aaf00a15c0f3ca25f.zip
Drop Promises
Diffstat (limited to 'ios/MullvadVPN/RelayCache')
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift329
1 files changed, 222 insertions, 107 deletions
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
index a42d8245df..fdb842afab 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
@@ -9,6 +9,7 @@
import BackgroundTasks
import Foundation
import Logging
+import UIKit
extension RelayCache {
@@ -28,8 +29,13 @@ extension RelayCache {
/// A dispatch queue used for thread synchronization
private let stateQueue = DispatchQueue(label: "RelayCacheTrackerStateQueue")
- /// A dispatch queue used for serializing relay cache updates
- private let updateQueue = DispatchQueue(label: "RelayCacheTrackerUpdateQueue")
+ /// Internal operation queue.
+ private let operationQueue: OperationQueue = {
+ let operationQueue = OperationQueue()
+ operationQueue.name = "RelayCacheTrackerQueue"
+ operationQueue.maxConcurrentOperationCount = 1
+ return operationQueue
+ }()
/// A timer source used for periodic updates
private var timerSource: DispatchSourceTimer?
@@ -60,7 +66,7 @@ extension RelayCache {
stateQueue.async {
guard !self.isPeriodicUpdatesEnabled else { return }
- self.logger.debug("Start periodic relay updates")
+ self.logger.debug("Start periodic relay updates.")
self.isPeriodicUpdatesEnabled = true
@@ -70,7 +76,7 @@ extension RelayCache {
self.scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow)
case .failure(let readError):
- self.logger.error(chainedError: readError, message: "Failed to read the relay cache")
+ self.logger.error(chainedError: readError, message: "Failed to read the relay cache.")
if Self.shouldDownloadRelaysOnReadFailure(readError) {
self.scheduleRepeatingTimer(startTime: .now())
@@ -83,7 +89,7 @@ extension RelayCache {
stateQueue.async {
guard self.isPeriodicUpdatesEnabled else { return }
- self.logger.debug("Stop periodic relay updates")
+ self.logger.debug("Stop periodic relay updates.")
self.isPeriodicUpdatesEnabled = false
@@ -92,34 +98,37 @@ extension RelayCache {
}
}
- func updateRelays() -> Result<RelayCache.FetchResult, RelayCache.Error>.Promise {
- return Promise.deferred {
- return RelayCache.IO.read(cacheFileURL: self.cacheFileURL)
- }
- .schedule(on: stateQueue)
- .then { result in
- switch result {
- case .success(let cachedRelays):
- let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
+ func updateRelays(completionHandler: @escaping (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void) -> AnyCancellable {
+ let operation = UpdateRelaysOperation(
+ dispatchQueue: stateQueue,
+ restClient: REST.Client.shared,
+ cacheFileURL: self.cacheFileURL,
+ relayUpdateInterval: Self.relayUpdateInterval,
+ updateHandler: { [weak self] newCachedRelays in
+ guard let self = self else { return }
- if nextUpdate <= Date() {
- return self.downloadRelays(previouslyCachedRelays: cachedRelays)
- } else {
- return .success(.throttled)
+ DispatchQueue.main.async {
+ self.observerList.forEach { observer in
+ observer.relayCache(self, didUpdateCachedRelays: newCachedRelays)
+ }
}
+ },
+ completionHandler: completionHandler
+ )
- case .failure(let readError):
- self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated")
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update relays") {
+ operation.cancel()
+ }
- if Self.shouldDownloadRelaysOnReadFailure(readError) {
- return self.downloadRelays(previouslyCachedRelays: nil)
- } else {
- return .failure(readError)
- }
- }
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
+
+ operationQueue.addOperation(operation)
+
+ return AnyCancellable {
+ operation.cancel()
}
- .block(on: updateQueue)
- .requestBackgroundTime(taskName: "RelayCacheTracker.updateRelays")
}
func read(completionHandler: @escaping (Result<CachedRelays, RelayCache.Error>) -> Void) {
@@ -133,13 +142,13 @@ extension RelayCache {
}
}
- func read() -> Result<CachedRelays, RelayCache.Error>.Promise {
- return Promise.deferred {
+ func readAndWait() -> Result<CachedRelays, RelayCache.Error> {
+ return stateQueue.sync {
return RelayCache.IO.readWithFallback(
cacheFileURL: self.cacheFileURL,
preBundledRelaysFileURL: self.prebundledRelaysFileURL
)
- }.schedule(on: stateQueue)
+ }
}
// MARK: - Observation
@@ -154,58 +163,12 @@ extension RelayCache {
// MARK: - Private instance methods
- private func downloadRelays(previouslyCachedRelays: CachedRelays?) -> Result<RelayCache.FetchResult, RelayCache.Error>.Promise {
- return REST.Client.shared.getRelays(etag: previouslyCachedRelays?.etag)
- .execute()
- .receive(on: stateQueue)
- .mapError { error in
- self.logger.error(chainedError: error, message: "Failed to download relays")
- return RelayCache.Error.rest(error)
- }
- .mapThen { result in
- switch result {
- case .newContent(let etag, let relays):
- let numRelays = relays.wireguard.relays.count
-
- self.logger.info("Downloaded \(numRelays) relays")
-
- let cachedRelays = CachedRelays(etag: etag, relays: relays, updatedAt: Date())
-
- return RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays)
- .asPromise()
- .map { _ in
- self.observerList.forEach { (observer) in
- observer.relayCache(self, didUpdateCachedRelays: cachedRelays)
- }
-
- return .newContent
- }
- .onFailure { error in
- self.logger.error(chainedError: error, message: "Failed to store downloaded relays")
- }
-
- case .notModified:
- self.logger.info("Relays haven't changed since last check.")
-
- var cachedRelays = previouslyCachedRelays!
- cachedRelays.updatedAt = Date()
-
- return RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays)
- .asPromise()
- .map { _ in
- return .sameContent
- }
- .onFailure { error in
- self.logger.error(chainedError: error, message: "Failed to update cached relays timestamp")
- }
- }
- }
- }
-
private func scheduleRepeatingTimer(startTime: DispatchWallTime) {
let timerSource = DispatchSource.makeTimerSource(queue: stateQueue)
timerSource.setEventHandler { [weak self] in
- self?.updateRelays().observe { _ in }
+ _ = self?.updateRelays(completionHandler: { _ in
+ // no-op
+ })
}
timerSource.schedule(wallDeadline: startTime, repeating: .seconds(Int(Self.relayUpdateInterval)))
@@ -273,15 +236,15 @@ extension RelayCache.Tracker {
}
if isRegistered {
- logger.debug("Registered app refresh task")
+ logger.debug("Registered app refresh task.")
} else {
- logger.error("Failed to register app refresh task")
+ logger.error("Failed to register app refresh task.")
}
}
/// Schedules app refresh task relative to the last relays update.
- func scheduleAppRefreshTask() -> Result<(), RelayCache.Error>.Promise {
- return self.read().flatMap { cachedRelays in
+ func scheduleAppRefreshTask() -> Result<(), RelayCache.Error> {
+ return readAndWait().flatMap { cachedRelays in
let beginDate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
return self.submitAppRefreshTask(at: beginDate)
@@ -303,34 +266,30 @@ extension RelayCache.Tracker {
/// Background task handler
private func handleAppRefreshTask(_ task: BGAppRefreshTask) {
- var cancellationToken: PromiseCancellationToken?
+ logger.debug("Start app refresh task.")
- self.logger.debug("Start app refresh task")
+ let cancellable = self.updateRelays { completion in
+ let isTaskCompleted: Bool
- self.updateRelays()
- .storeCancellationToken(in: &cancellationToken)
- .observe { completion in
- let isTaskCompleted: Bool
+ switch completion {
+ case .success(let fetchResult):
+ self.logger.debug("Finished updating relays in app refresh task: \(fetchResult).")
+ isTaskCompleted = true
- switch completion {
- case .finished(.success(let fetchResult)):
- self.logger.debug("Finished updating relays in app refresh task: \(fetchResult)")
- isTaskCompleted = true
-
- case .finished(.failure(let error)):
- self.logger.error(chainedError: error, message: "Failed to update relays in app refresh task")
- isTaskCompleted = false
-
- case .cancelled:
- self.logger.debug("App refresh task was cancelled")
- isTaskCompleted = false
- }
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to update relays in app refresh task.")
+ isTaskCompleted = false
- task.setTaskCompleted(success: isTaskCompleted)
+ case .cancelled:
+ self.logger.debug("App refresh task was cancelled.")
+ isTaskCompleted = false
}
+ task.setTaskCompleted(success: isTaskCompleted)
+ }
+
task.expirationHandler = {
- cancellationToken?.cancel()
+ cancellable.cancel()
}
// Schedule next refresh
@@ -338,10 +297,166 @@ extension RelayCache.Tracker {
switch self.submitAppRefreshTask(at: scheduleDate) {
case .success:
- self.logger.debug("Scheduled next app refresh task at \(scheduleDate.logFormatDate())")
+ logger.debug("Scheduled next app refresh task at \(scheduleDate.logFormatDate()).")
+
+ case .failure(let error):
+ logger.error(chainedError: error, message: "Failed to schedule next app refresh task.")
+ }
+ }
+}
+
+fileprivate class UpdateRelaysOperation: AsyncOperation {
+ typealias UpdateHandler = (RelayCache.CachedRelays) -> Void
+ typealias CompletionHandler = (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void
+
+ private let dispatchQueue: DispatchQueue
+ private let restClient: REST.Client
+ private let cacheFileURL: URL
+ private let relayUpdateInterval: TimeInterval
+
+ private let logger = Logger(label: "RelayCacheTracker.UpdateRelaysOperation")
+
+ private let updateHandler: UpdateHandler
+ private var completionHandler: CompletionHandler?
+ private var downloadCancellable: AnyCancellable?
+
+ init(dispatchQueue: DispatchQueue,
+ restClient: REST.Client,
+ cacheFileURL: URL,
+ relayUpdateInterval: TimeInterval,
+ updateHandler: @escaping UpdateHandler,
+ completionHandler: @escaping CompletionHandler) {
+ self.dispatchQueue = dispatchQueue
+ self.restClient = restClient
+ self.cacheFileURL = cacheFileURL
+ self.relayUpdateInterval = relayUpdateInterval
+ self.updateHandler = updateHandler
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ dispatchQueue.async {
+ guard !self.isCancelled else {
+ self.finish(completion: .cancelled)
+ return
+ }
+
+ let readResult = RelayCache.IO.read(cacheFileURL: self.cacheFileURL)
+ switch readResult {
+ case .success(let cachedRelays):
+ let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(self.relayUpdateInterval)
+
+ if nextUpdate <= Date() {
+ self.downloadRelays(previouslyCachedRelays: cachedRelays)
+ } else {
+ self.finish(completion: .success(.throttled))
+ }
+
+ case .failure(let readError):
+ self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated.")
+
+ if self.shouldDownloadRelaysOnReadFailure(readError) {
+ self.downloadRelays(previouslyCachedRelays: nil)
+ } else {
+ self.finish(completion: .failure(readError))
+ }
+ }
+ }
+ }
+
+ override func cancel() {
+ super.cancel()
+
+ dispatchQueue.async {
+ self.downloadCancellable?.cancel()
+ }
+ }
+
+ private func finish(completion: OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) {
+ let block = completionHandler
+ completionHandler = nil
+
+ block?(completion)
+
+ finish()
+ }
+
+ private func didReceiveNewRelays(etag: String?, relays: REST.ServerRelaysResponse) {
+ let numRelays = relays.wireguard.relays.count
+
+ logger.info("Downloaded \(numRelays) relays.")
+
+ let cachedRelays = RelayCache.CachedRelays(etag: etag, relays: relays, updatedAt: Date())
+ let writeResult = RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays)
+
+ switch writeResult {
+ case .success:
+ updateHandler(cachedRelays)
+
+ finish(completion: .success(.newContent))
+
+ case .failure(let error):
+ logger.error(chainedError: error, message: "Failed to store downloaded relays.")
+
+ finish(completion: .failure(.writeCache(error)))
+ }
+ }
+
+ private func didReceiveNotModified(previouslyCachedRelays: RelayCache.CachedRelays) {
+ logger.info("Relays haven't changed since last check.")
+
+ var cachedRelays = previouslyCachedRelays
+ cachedRelays.updatedAt = Date()
+
+ let writeResult = RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays)
+
+ switch writeResult {
+ case .success:
+ finish(completion: .success(.sameContent))
case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to schedule next app refresh task")
+ logger.error(chainedError: error, message: "Failed to update cached relays timestamp.")
+
+ finish(completion: .failure(.writeCache(error)))
+ }
+ }
+
+ private func didFailToDownloadRelays(error: REST.Error) {
+ logger.error(chainedError: error, message: "Failed to download relays.")
+
+ finish(completion: .failure(.rest(error)))
+ }
+
+ private func downloadRelays(previouslyCachedRelays: RelayCache.CachedRelays?) {
+ downloadCancellable = REST.Client.shared.getRelays(etag: previouslyCachedRelays?.etag)
+ .execute { [weak self] result in
+ guard let self = self else { return }
+
+ self.dispatchQueue.async {
+ switch result {
+ case .success(.newContent(let etag, let relays)):
+ self.didReceiveNewRelays(etag: etag, relays: relays)
+
+ case .success(.notModified):
+ self.didReceiveNotModified(previouslyCachedRelays: previouslyCachedRelays!)
+
+ case .failure(let error):
+ self.didFailToDownloadRelays(error: error)
+ }
+ }
+ }
+ }
+
+ private func shouldDownloadRelaysOnReadFailure(_ error: RelayCache.Error) -> Bool {
+ switch error {
+ case .readPrebundledRelays, .decodePrebundledRelays, .decodeCache:
+ return true
+
+ case .readCache(CocoaError.fileReadNoSuchFile):
+ return true
+
+ default:
+ return false
}
}
}