From 49ff87f3cc09ff1fc60a3313dee6921c09bcbaea Mon Sep 17 00:00:00 2001 From: Harry <109690906+harry-signal@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:41:29 -0800 Subject: [PATCH] Break up link'n'sync long polls into shorter repeated requests --- .../Devices/LinkAndSyncManager.swift | 94 ++++++++++++++----- SignalServiceKit/Environment/AppSetup.swift | 1 + SignalServiceKit/Util/FeatureFlags.swift | 9 +- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/SignalServiceKit/Devices/LinkAndSyncManager.swift b/SignalServiceKit/Devices/LinkAndSyncManager.swift index afbeb3961c..385caad212 100644 --- a/SignalServiceKit/Devices/LinkAndSyncManager.swift +++ b/SignalServiceKit/Devices/LinkAndSyncManager.swift @@ -136,6 +136,7 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { private let appContext: AppContext private let attachmentDownloadManager: AttachmentDownloadManager private let attachmentUploadManager: AttachmentUploadManager + private let dateProvider: DateProvider private let db: any DB private let kvStore: KeyValueStore private let messageBackupManager: MessageBackupManager @@ -147,6 +148,7 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { appContext: AppContext, attachmentDownloadManager: AttachmentDownloadManager, attachmentUploadManager: AttachmentUploadManager, + dateProvider: @escaping DateProvider, db: any DB, messageBackupManager: MessageBackupManager, messagePipelineSupervisor: MessagePipelineSupervisor, @@ -156,6 +158,7 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { self.appContext = appContext self.attachmentDownloadManager = attachmentDownloadManager self.attachmentUploadManager = attachmentUploadManager + self.dateProvider = dateProvider self.db = db self.kvStore = KeyValueStore(collection: "LinkAndSyncManagerImpl") self.messageBackupManager = messageBackupManager @@ -410,18 +413,37 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { private func _waitForDeviceToLink( tokenId: DeviceProvisioningTokenId ) async throws(PrimaryLinkNSyncError) -> Requests.WaitForDeviceToLinkResponse { - let response: HTTPResponse - do { - Logger.info("Waiting for device to link") - response = try await networkManager.asyncRequest( - Requests.waitForDeviceToLink(tokenId: tokenId), - canUseWebSocket: false - ) - Logger.info("Device linked!") - } catch { - if error is CancellationError { - throw .cancelled(linkedDeviceId: nil) + Logger.info("Waiting for device to link") + let startDate = dateProvider() + var numNetworkErrors = 0 + var response: HTTPResponse? + while dateProvider().timeIntervalSince(startDate) < Constants.waitForDeviceLinkTotalTimeout { + do { + response = try await networkManager.asyncRequest( + Requests.waitForDeviceToLink(tokenId: tokenId), + canUseWebSocket: false + ) + if response?.responseStatusCode == Requests.WaitForDeviceToLinkResponseCodes.timeout.rawValue { + // retry + continue + } + Logger.info("Device linked!") + break + } catch { + if error is CancellationError { + throw .cancelled(linkedDeviceId: nil) + } + if error .isNetworkFailureOrTimeout { + numNetworkErrors += 1 + if numNetworkErrors <= 3 { + // retry + continue + } + } + throw .errorWaitingForLinkedDevice } + } + guard let response else { throw .errorWaitingForLinkedDevice } @@ -619,16 +641,35 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { private func _waitForPrimaryToUploadBackup( auth: ChatServiceAuth ) async throws(SecondaryLinkNSyncError) -> Requests.ExportAndUploadBackupResult { - let response: HTTPResponse - do { - response = try await networkManager.asyncRequest( - Requests.waitForLinkNSyncBackupUpload(auth: auth), - canUseWebSocket: false - ) - } catch { - if error is CancellationError { - throw SecondaryLinkNSyncError.cancelled + let startDate = dateProvider() + var numNetworkErrors = 0 + var response: HTTPResponse? + while dateProvider().timeIntervalSince(startDate) < Constants.waitForBackupUploadTotalTimeout { + do { + response = try await networkManager.asyncRequest( + Requests.waitForLinkNSyncBackupUpload(auth: auth), + canUseWebSocket: false + ) + if response?.responseStatusCode == Requests.WaitForLinkNSyncBackupUploadResponseCodes.timeout.rawValue { + continue + } + break + } catch { + if error is CancellationError { + throw SecondaryLinkNSyncError.cancelled + } + + if error .isNetworkFailureOrTimeout { + numNetworkErrors += 1 + if numNetworkErrors <= 3 { + // retry + continue + } + } + throw SecondaryLinkNSyncError.networkError } + } + guard let response else { throw SecondaryLinkNSyncError.networkError } @@ -724,8 +765,9 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { static let enabledOnPrimaryKey = "enabledOnPrimaryKey" - static let waitForDeviceLinkTimeoutSeconds: UInt32 = FeatureFlags.linkAndSyncTimeoutSeconds - static let waitForBackupUploadTimeoutSeconds: UInt32 = FeatureFlags.linkAndSyncTimeoutSeconds + static let longPollRequestTimeoutSeconds: UInt32 = 10 + static let waitForDeviceLinkTotalTimeout: TimeInterval = FeatureFlags.linkAndSyncTotalPrimaryTimeout + static let waitForBackupUploadTotalTimeout: TimeInterval = FeatureFlags.linkAndSyncTotalSecondaryTimeout } // MARK: - @@ -757,7 +799,7 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { var urlComponents = URLComponents(string: "v1/devices/wait_for_linked_device/\(tokenId.id)")! urlComponents.queryItems = [URLQueryItem( name: "timeout", - value: "\(LinkAndSyncManagerImpl.Constants.waitForDeviceLinkTimeoutSeconds)" + value: "\(LinkAndSyncManagerImpl.Constants.longPollRequestTimeoutSeconds)" )] let request = TSRequest( url: urlComponents.url!, @@ -767,7 +809,7 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { request.shouldHaveAuthorizationHeaders = true request.applyRedactionStrategy(.redactURLForSuccessResponses()) // The timeout is server side; apply wiggle room for our local clock. - request.timeoutInterval = 30 + TimeInterval(Constants.waitForDeviceLinkTimeoutSeconds) + request.timeoutInterval = 10 + TimeInterval(Constants.longPollRequestTimeoutSeconds) return request } @@ -834,7 +876,7 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { var urlComponents = URLComponents(string: "v1/devices/transfer_archive")! urlComponents.queryItems = [URLQueryItem( name: "timeout", - value: "\(Constants.waitForBackupUploadTimeoutSeconds)" + value: "\(Constants.longPollRequestTimeoutSeconds)" )] let request = TSRequest( url: urlComponents.url!, @@ -845,7 +887,7 @@ public class LinkAndSyncManagerImpl: LinkAndSyncManager { request.setAuth(auth) request.applyRedactionStrategy(.redactURLForSuccessResponses()) // The timeout is server side; apply wiggle room for our local clock. - request.timeoutInterval = 30 + TimeInterval(Constants.waitForBackupUploadTimeoutSeconds) + request.timeoutInterval = 10 + TimeInterval(Constants.longPollRequestTimeoutSeconds) return request } } diff --git a/SignalServiceKit/Environment/AppSetup.swift b/SignalServiceKit/Environment/AppSetup.swift index f1fd930f61..45c43441a5 100644 --- a/SignalServiceKit/Environment/AppSetup.swift +++ b/SignalServiceKit/Environment/AppSetup.swift @@ -1225,6 +1225,7 @@ public class AppSetup { appContext: appContext, attachmentDownloadManager: attachmentDownloadManager, attachmentUploadManager: attachmentUploadManager, + dateProvider: dateProvider, db: db, messageBackupManager: messageBackupManager, messagePipelineSupervisor: messagePipelineSupervisor, diff --git a/SignalServiceKit/Util/FeatureFlags.swift b/SignalServiceKit/Util/FeatureFlags.swift index c44e2ad078..cc9fb28a67 100644 --- a/SignalServiceKit/Util/FeatureFlags.swift +++ b/SignalServiceKit/Util/FeatureFlags.swift @@ -43,10 +43,13 @@ public enum FeatureFlags { public static let messageBackupRemoteExportAlpha = build.includes(.dev) public static let messageBackupFileAlphaRegistrationFlow = build.includes(.dev) public static let linkAndSync = build.includes(.beta) - public static let linkAndSyncTimeoutSeconds: UInt32 = switch build { + public static let linkAndSyncTotalPrimaryTimeout: TimeInterval = switch build { + case .dev, .internal: 120 + case .beta, .production: 60 + } + public static let linkAndSyncTotalSecondaryTimeout: TimeInterval = switch build { case .dev, .internal: 300 - case .beta: 150 - case .production: 60 + case .beta, .production: 120 } public static let backupsMemorySamplerRatio: Float = build.includes(.internal) ? 0.1 : 0 public static let messageBackupQuickRestoreFlow = false