diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift index 0493edc85..2d480bf59 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI+SwarmAPI.swift @@ -41,7 +41,7 @@ public extension LokiAPI { ] print("[Loki] Populating snode pool using: \(target).") let (promise, seal) = Promise.pending() - attempt(maxRetryCount: 4) { + attempt(maxRetryCount: 4, recoveringOn: LokiAPI.workQueue) { HTTP.execute(.post, url, parameters: parameters).map2 { json -> LokiAPITarget in guard let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed } snodePool = try Set(rawTargets.flatMap { rawTarget in diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index b085186fe..e66766774 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -61,7 +61,7 @@ public final class LokiAPI : NSObject { // MARK: Public API public static func getMessages() -> Promise> { - return attempt(maxRetryCount: maxRetryCount) { + return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { getTargetSnodes(for: getUserHexEncodedPublicKey()).mapValues2 { targetSnode in getRawMessages(from: targetSnode, usingLongPolling: false).map2 { parseRawMessagesResponse($0, from: targetSnode) } }.map2 { Set($0) } @@ -89,7 +89,7 @@ public final class LokiAPI : NSObject { let parameters = lokiMessageWithPoW.toJSON() return Set(snodes.map { snode in // Send the message to the target snode - return attempt(maxRetryCount: maxRetryCount) { + return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { invoke(.sendMessage, on: snode, associatedWith: destination, parameters: parameters) }.map2 { rawResponse in if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int { diff --git a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift index b78a05eab..9e638ff77 100644 --- a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift @@ -98,7 +98,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI { let url = URL(string: "\(server)/users/me")! let request = TSRequest(url: url, method: "PATCH", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] - return attempt(maxRetryCount: 8) { + return attempt(maxRetryCount: 8, recoveringOn: LokiAPI.workQueue) { LokiFileServerProxy(for: server).perform(request).map2 { _ in } }.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in print("Couldn't update device links due to error: \(error).") diff --git a/SignalServiceKit/src/Loki/API/LokiPoller.swift b/SignalServiceKit/src/Loki/API/LokiPoller.swift index 52efd639a..c6dd2e6ac 100644 --- a/SignalServiceKit/src/Loki/API/LokiPoller.swift +++ b/SignalServiceKit/src/Loki/API/LokiPoller.swift @@ -10,7 +10,7 @@ public final class LokiPoller : NSObject { private var pollCount = 0 // MARK: Settings - private static let retryInterval: TimeInterval = 1 + private static let retryInterval: TimeInterval = 0.25 /// After polling a given snode this many times we always switch to a new one. /// /// The reason for doing this is that sometimes a snode will be giving us successful responses while diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index dcfdc7e19..2b0a0c2c2 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -83,7 +83,9 @@ public enum OnionRequestAPI { unusedSnodes.remove(candidate) // All used snodes should be unique print("[Loki] [Onion Request API] Testing guard snode: \(candidate).") // Loop until a reliable guard snode is found - return testSnode(candidate).map2 { candidate }.recover2 { _ in getGuardSnode() } + return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in + withDelay(0.25, completionQueue: LokiAPI.workQueue) { getGuardSnode() } + } } let promises = (0...pending() DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(LokiDotNetAPIError.signingFailed) } - attempt(maxRetryCount: maxRetryCount) { + attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { getAuthToken(for: server).then2 { token -> Promise in let url = URL(string: "\(server)/channels/\(channel)/messages")! let parameters = signedMessage.toJSON() @@ -244,7 +244,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { let isModerationRequest = !isSentByUser print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).") let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)" - return attempt(maxRetryCount: maxRetryCount) { + return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { getAuthToken(for: server).then2 { token -> Promise in let url = URL(string: urlAsString)! let request = TSRequest(url: url, method: "DELETE", parameters: [:]) @@ -292,7 +292,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise { print("[Loki] Updating display name on server: \(server).") let parameters: JSON = [ "name" : (newDisplayName ?? "") ] - return attempt(maxRetryCount: maxRetryCount) { + return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { getAuthToken(for: server).then2 { token -> Promise in let url = URL(string: "\(server)/users/me")! let request = TSRequest(url: url, method: "PATCH", parameters: parameters) @@ -317,7 +317,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { annotation["value"] = [ "profileKey" : profileKey.base64EncodedString(), "url" : url ] } let parameters: JSON = [ "annotations" : [ annotation ] ] - return attempt(maxRetryCount: maxRetryCount) { + return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { getAuthToken(for: server).then2 { token -> Promise in let url = URL(string: "\(server)/users/me")! let request = TSRequest(url: url, method: "PATCH", parameters: parameters) @@ -382,7 +382,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } public static func getInfo(for channel: UInt64, on server: String) -> Promise { - return attempt(maxRetryCount: maxRetryCount) { + return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { getAuthToken(for: server).then2 { token -> Promise in let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")! let request = TSRequest(url: url) @@ -413,7 +413,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } public static func join(_ channel: UInt64, on server: String) -> Promise { - return attempt(maxRetryCount: maxRetryCount) { + return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { getAuthToken(for: server).then2 { token -> Promise in let url = URL(string: "\(server)/channels/\(channel)/subscribe")! let request = TSRequest(url: url, method: "POST", parameters: [:]) @@ -426,7 +426,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } public static func leave(_ channel: UInt64, on server: String) -> Promise { - return attempt(maxRetryCount: maxRetryCount) { + return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) { getAuthToken(for: server).then2 { token -> Promise in let url = URL(string: "\(server)/channels/\(channel)/subscribe")! let request = TSRequest(url: url, method: "DELETE", parameters: [:]) diff --git a/SignalServiceKit/src/Loki/Utilities/Promise+Delaying.swift b/SignalServiceKit/src/Loki/Utilities/Promise+Delaying.swift new file mode 100644 index 000000000..533ee70e7 --- /dev/null +++ b/SignalServiceKit/src/Loki/Utilities/Promise+Delaying.swift @@ -0,0 +1,11 @@ +import PromiseKit + +/// Delay the execution of the promise constructed in `body` by `delay` seconds. +internal func withDelay(_ delay: TimeInterval, completionQueue: DispatchQueue, body: @escaping () -> Promise) -> Promise { + AssertIsOnMainThread() // Timers don't do well on background queues + let (promise, seal) = Promise.pending() + Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { _ in + body().done(on: completionQueue) { seal.fulfill($0) }.catch(on: completionQueue) { seal.reject($0) } + } + return promise +} diff --git a/SignalServiceKit/src/Loki/Utilities/Promise+Retrying.swift b/SignalServiceKit/src/Loki/Utilities/Promise+Retrying.swift index 675c25782..a386b23fd 100644 --- a/SignalServiceKit/src/Loki/Utilities/Promise+Retrying.swift +++ b/SignalServiceKit/src/Loki/Utilities/Promise+Retrying.swift @@ -1,7 +1,7 @@ import PromiseKit /// Retry the promise constructed in `body` up to `maxRetryCount` times. -internal func attempt(maxRetryCount: UInt, recoveringOn queue: DispatchQueue = .global(qos: .default), body: @escaping () -> Promise) -> Promise { +internal func attempt(maxRetryCount: UInt, recoveringOn queue: DispatchQueue, body: @escaping () -> Promise) -> Promise { var retryCount = 0 func attempt() -> Promise { return body().recover(on: queue) { error -> Promise in