diff --git a/UnitTests/SwiftTests/MPTransportErrorDetectorTests.swift b/UnitTests/SwiftTests/MPTransportErrorDetectorTests.swift new file mode 100644 index 000000000..126dafa36 --- /dev/null +++ b/UnitTests/SwiftTests/MPTransportErrorDetectorTests.swift @@ -0,0 +1,95 @@ +import XCTest +internal import mParticle_Apple_SDK_Swift + +final class MPTransportErrorDetectorTests: XCTestCase { + override func setUp() { + super.setUp() + MPTransportErrorDetector.resetTransportErrorCounter() + } + + func test_isRetriableTransportError_returnsFalse_whenErrorIsNil() { + XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(nil)) + } + + func test_isRetriableTransportError_returnsFalse_whenNoConnectionCode() { + let error = NSError(domain: "any-domain", code: 1) + XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(error)) + } + + func test_isRetriableTransportError_returnsTrue_whenNSURLErrorIsRetriable() { + let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost) + XCTAssertTrue(MPTransportErrorDetector.isRetriableTransportError(error)) + } + + func test_isRetriableTransportError_returnsFalse_whenNSURLErrorIsNotRetriable() { + let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled) + XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(error)) + } + + func test_isRetriableTransportError_returnsTrue_whenNSURLErrorIsNotConnectedToInternet() { + let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet) + XCTAssertTrue(MPTransportErrorDetector.isRetriableTransportError(error)) + } + + func test_isRetriableTransportError_returnsTrue_whenNSURLErrorIsTimedOut() { + let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut) + XCTAssertTrue(MPTransportErrorDetector.isRetriableTransportError(error)) + } + + func test_isRetriableTransportError_returnsFalse_whenNSURLErrorIsAppTransportSecurityRequiresSecureConnection() { + let error = NSError( + domain: NSURLErrorDomain, + code: NSURLErrorAppTransportSecurityRequiresSecureConnection + ) + XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(error)) + } + + func test_isRetriableTransportError_returnsTrue_forMParticleTimeoutError() { + let error = NSError( + domain: MPTransportErrorDetector.semaphoreTimeoutErrorDomain() as String, + code: MPTransportErrorDetector.semaphoreTimeoutErrorCode().intValue + ) + XCTAssertTrue(MPTransportErrorDetector.isRetriableTransportError(error)) + } + + func test_isRetriableTransportError_returnsFalse_forUnknownError() { + let error = NSError(domain: "custom-domain", code: 42) + XCTAssertFalse(MPTransportErrorDetector.isRetriableTransportError(error)) + } + + func test_calculateRetryTimeForTransportError_usesSmallValueForFirstError() { + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 5) + } + + func test_calculateRetryTimeForTransportError_reachesMaxAtFiveErrors() { + for _ in 0..<4 { + _ = MPTransportErrorDetector.calculateRetryTimeForTransportError() + } + + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 300) + } + + func test_calculateRetryTimeForTransportError_usesExpectedBackoffSchedule() { + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 5) + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 15) + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 60) + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 120) + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 300) + } + + func test_calculateRetryTimeForTransportError_staysAtMaxAfterThreshold() { + for _ in 0..<5 { + _ = MPTransportErrorDetector.calculateRetryTimeForTransportError() + } + + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 300) + } + + func test_resetTransportErrorCounter_resetsBackoff() { + _ = MPTransportErrorDetector.calculateRetryTimeForTransportError() + _ = MPTransportErrorDetector.calculateRetryTimeForTransportError() + MPTransportErrorDetector.resetTransportErrorCounter() + + XCTAssertEqual(MPTransportErrorDetector.calculateRetryTimeForTransportError().doubleValue, 5) + } +} diff --git a/mParticle-Apple-SDK-Swift/Sources/Network/MPNetworkCommunicationHelper.swift b/mParticle-Apple-SDK-Swift/Sources/Network/MPNetworkCommunicationHelper.swift new file mode 100644 index 000000000..71ecafd7c --- /dev/null +++ b/mParticle-Apple-SDK-Swift/Sources/Network/MPNetworkCommunicationHelper.swift @@ -0,0 +1,23 @@ +import Foundation + +@objc public final class MPNetworkCommunicationHelper: NSObject { + @objc(calculateRetryTimeForHeaders:) + public static func calculateRetryTime(for headers: NSDictionary) -> NSNumber { + let retryAfterDate = headers.mp_retryDate() + let retryAfterSeconds = headers.mp_retrySeconds() + let defaultRetryAfter: Double = 7200 + let maxRetryAfter: Double = 86400 + + if let retryAfterDate { + let now = Date() + let retryAfter = min(retryAfterDate.timeIntervalSince1970 - now.timeIntervalSince1970, maxRetryAfter) + return NSNumber(value: retryAfter > 0 ? retryAfter : defaultRetryAfter) + } + + if let retryAfterSeconds { + return NSNumber(value: min(retryAfterSeconds.doubleValue, maxRetryAfter)) + } + + return NSNumber(value: defaultRetryAfter) + } +} diff --git a/mParticle-Apple-SDK-Swift/Sources/Network/MPTransportErrorDetector.swift b/mParticle-Apple-SDK-Swift/Sources/Network/MPTransportErrorDetector.swift new file mode 100644 index 000000000..59945b83c --- /dev/null +++ b/mParticle-Apple-SDK-Swift/Sources/Network/MPTransportErrorDetector.swift @@ -0,0 +1,78 @@ +import Foundation + +@objc public class MPTransportErrorDetector: NSObject { + private static let maxRetryAfter: Double = 300 + private static let maxErrorCountBeforeMaxRetry = 5 + private static let retryAfterSchedule: [Double] = [5, 15, 60, 120, 300] + private static var consecutiveTransportErrorCount = 0 + private static let backoffQueue = DispatchQueue(label: "com.mparticle.transport-error-backoff") + private static let semaphoreTimeoutErrorDomainValue = "com.mparticle" + private static let semaphoreTimeoutErrorCodeValue = 0 + + @objc(semaphoreTimeoutErrorDomain) + public static func semaphoreTimeoutErrorDomain() -> NSString { + semaphoreTimeoutErrorDomainValue as NSString + } + + @objc(semaphoreTimeoutErrorCode) + public static func semaphoreTimeoutErrorCode() -> NSNumber { + NSNumber(value: semaphoreTimeoutErrorCodeValue) + } + + @objc(isRetriableTransportError:) + public static func isRetriableTransportError(_ error: NSError?) -> Bool { + guard let error else { + return false + } + + if error.domain == NSURLErrorDomain { + switch error.code { + case NSURLErrorCannotFindHost, + NSURLErrorCannotConnectToHost, + NSURLErrorNotConnectedToInternet, + NSURLErrorNetworkConnectionLost, + NSURLErrorDNSLookupFailed, + NSURLErrorTimedOut, + NSURLErrorCannotLoadFromNetwork, + NSURLErrorSecureConnectionFailed, + NSURLErrorInternationalRoamingOff, + NSURLErrorDataNotAllowed, + NSURLErrorCallIsActive: + return true + default: + return false + } + } + + return error.domain == semaphoreTimeoutErrorDomainValue + && error.code == semaphoreTimeoutErrorCodeValue + } + + @objc(calculateRetryTimeForTransportError) + public static func calculateRetryTimeForTransportError() -> NSNumber { + return backoffQueue.sync { + consecutiveTransportErrorCount += 1 + + if consecutiveTransportErrorCount >= maxErrorCountBeforeMaxRetry { + return NSNumber(value: maxRetryAfter) + } + + guard !retryAfterSchedule.isEmpty else { + return NSNumber(value: maxRetryAfter) + } + + let scheduleIndex = min( + max(0, consecutiveTransportErrorCount - 1), + retryAfterSchedule.count - 1 + ) + return NSNumber(value: retryAfterSchedule[scheduleIndex]) + } + } + + @objc(resetTransportErrorCounter) + public static func resetTransportErrorCounter() { + backoffQueue.sync { + consecutiveTransportErrorCount = 0 + } + } +} diff --git a/mParticle-Apple-SDK-Swift/Sources/Network/NSDictionary+MPRetryAfter.swift b/mParticle-Apple-SDK-Swift/Sources/Network/NSDictionary+MPRetryAfter.swift new file mode 100644 index 000000000..4156ac091 --- /dev/null +++ b/mParticle-Apple-SDK-Swift/Sources/Network/NSDictionary+MPRetryAfter.swift @@ -0,0 +1,26 @@ +import Foundation + +@objc public extension NSDictionary { + @objc(mp_retryDate) + func mp_retryDate() -> Date? { + guard let headerValue = self["Retry-After"] as? String else { + return nil + } + + return MPDateFormatter.date(fromStringRFC1123: headerValue) + } + + @objc(mp_retrySeconds) + func mp_retrySeconds() -> NSNumber? { + let headerValue = self["Retry-After"] + + if let number = headerValue as? NSNumber { + return NSNumber(value: number.doubleValue) + } + if let string = headerValue as? String { + return Double(string).map { NSNumber(value: $0) } + } + + return nil + } +} diff --git a/mParticle-Apple-SDK-Swift/Test/Utils/MPNetworkCommunicationHelperTests.swift b/mParticle-Apple-SDK-Swift/Test/Utils/MPNetworkCommunicationHelperTests.swift new file mode 100644 index 000000000..5dfabdf47 --- /dev/null +++ b/mParticle-Apple-SDK-Swift/Test/Utils/MPNetworkCommunicationHelperTests.swift @@ -0,0 +1,50 @@ +import XCTest +@testable import mParticle_Apple_SDK_Swift + +final class MPNetworkCommunicationHelperTests: XCTestCase { + func testCalculateRetryTimeUsesNumberHeader() { + let headers: NSDictionary = ["Retry-After": NSNumber(value: 42)] + + XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 42) + } + + func testCalculateRetryTimeUsesStringSecondsHeader() { + let headers: NSDictionary = ["Retry-After": "123.5"] + + XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 123.5) + } + + func testCalculateRetryTimeFallsBackToDefaultForInvalidString() { + let headers: NSDictionary = ["Retry-After": "not-a-number"] + + XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 7200) + } + + func testCalculateRetryTimeFallsBackToDefaultWhenHeaderMissing() { + let headers: NSDictionary = ["Other-Header": "1"] + + XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 7200) + } + + func testCalculateRetryTimeClampsToMax() { + let headers: NSDictionary = ["Retry-After": NSNumber(value: 999999)] + + XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 86400) + } + + func testCalculateRetryTimeParsesDateHeader() { + let futureDate = Date().addingTimeInterval(300) + let retryAfterHeader = MPDateFormatter.string(fromDateRFC1123: futureDate) + let headers: NSDictionary = ["Retry-After": retryAfterHeader as Any] + + XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 300, accuracy: 2.0) + } + + func testCalculateRetryTimeUsesDefaultForPastDateHeader() { + let pastDate = Date().addingTimeInterval(-300) + let retryAfterHeader = MPDateFormatter.string(fromDateRFC1123: pastDate) + let headers: NSDictionary = ["Retry-After": retryAfterHeader as Any] + + XCTAssertEqual(MPNetworkCommunicationHelper.calculateRetryTime(for: headers).doubleValue, 7200) + } +} diff --git a/mParticle-Apple-SDK-Swift/Test/Utils/NSDictionary+MPRetryAfterTests.swift b/mParticle-Apple-SDK-Swift/Test/Utils/NSDictionary+MPRetryAfterTests.swift new file mode 100644 index 000000000..31b663a02 --- /dev/null +++ b/mParticle-Apple-SDK-Swift/Test/Utils/NSDictionary+MPRetryAfterTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import mParticle_Apple_SDK_Swift + +final class NSDictionaryMPRetryAfterTests: XCTestCase { + func testRetryDateParsesRFC1123String() { + let headers: NSDictionary = ["Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT"] + + XCTAssertNotNil(headers.mp_retryDate()) + } + + func testRetryDateReturnsNilForInvalidString() { + let headers: NSDictionary = ["Retry-After": "not-a-date"] + + XCTAssertNil(headers.mp_retryDate()) + } + + func testRetrySecondsReturnsNumberValue() { + let headers: NSDictionary = ["Retry-After": NSNumber(value: 42)] + + XCTAssertEqual(headers.mp_retrySeconds()?.doubleValue, 42) + } + + func testRetrySecondsParsesStringValue() { + let headers: NSDictionary = ["Retry-After": "123.5"] + + XCTAssertEqual(headers.mp_retrySeconds()?.doubleValue, 123.5) + } + + func testRetrySecondsReturnsNilForInvalidString() { + let headers: NSDictionary = ["Retry-After": "not-a-number"] + + XCTAssertNil(headers.mp_retrySeconds()) + } + + func testRetrySecondsReturnsNilWhenHeaderMissing() { + let headers: NSDictionary = ["Other-Header": "1"] + + XCTAssertNil(headers.mp_retrySeconds()) + } +} diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index 053fc4088..e742395d9 100644 --- a/mParticle-Apple-SDK.xcodeproj/project.pbxproj +++ b/mParticle-Apple-SDK.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 359BAFFC2E56335300A8A704 /* SettingsProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 359BAFFB2E56335300A8A704 /* SettingsProvider.m */; }; 359BAFFF2E575B0300A8A704 /* SettingsProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359BAFFE2E575AF500A8A704 /* SettingsProviderTests.swift */; }; 359BB0062E57A56800A8A704 /* MPUserDefaultsMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359BB0052E57A56300A8A704 /* MPUserDefaultsMock.swift */; }; + 35A1B2C32F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A1B2C22F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift */; }; 35C3DE702F2919B40077C0FD /* MPCCPAConsent.m in Sources */ = {isa = PBXBuildFile; fileRef = 35C3DE6F2F2919B40077C0FD /* MPCCPAConsent.m */; }; 35C3DE712F2919B40077C0FD /* MPCCPAConsent.h in Headers */ = {isa = PBXBuildFile; fileRef = 35C3DE6E2F2919B40077C0FD /* MPCCPAConsent.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35C3DE732F291DA40077C0FD /* MPGDPRConsent.m in Sources */ = {isa = PBXBuildFile; fileRef = 35C3DE722F291DA40077C0FD /* MPGDPRConsent.m */; }; @@ -346,6 +347,7 @@ 359BAFFB2E56335300A8A704 /* SettingsProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsProvider.m; sourceTree = ""; }; 359BAFFE2E575AF500A8A704 /* SettingsProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProviderTests.swift; sourceTree = ""; }; 359BB0052E57A56300A8A704 /* MPUserDefaultsMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPUserDefaultsMock.swift; sourceTree = ""; }; + 35A1B2C22F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPTransportErrorDetectorTests.swift; sourceTree = ""; }; 35C3DE6E2F2919B40077C0FD /* MPCCPAConsent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPCCPAConsent.h; sourceTree = ""; }; 35C3DE6F2F2919B40077C0FD /* MPCCPAConsent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPCCPAConsent.m; sourceTree = ""; }; 35C3DE722F291DA40077C0FD /* MPGDPRConsent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPGDPRConsent.m; sourceTree = ""; }; @@ -630,11 +632,13 @@ Test/Utils/MPDateFormatterTests.swift, Test/Utils/MPDeviceTests.swift, Test/Utils/MPLaunchInfoTests.m, + Test/Utils/MPNetworkCommunicationHelperTests.swift, Test/Utils/MPResponseConfigTests.swift, Test/Utils/MPUserAttributeChangeTests.m, Test/Utils/MPUserDefaultsTests.swift, Test/Utils/MPZipTests.m, "Test/Utils/NSArray+MPCaseInsensitiveTests.swift", + "Test/Utils/NSDictionary+MPRetryAfterTests.swift", "Test/Utils/NSNumber+MPFormatterTests.m", "Test/Utils/NSString+MPPercentEscapeTests.swift", ); @@ -649,11 +653,13 @@ Test/Utils/MPDateFormatterTests.swift, Test/Utils/MPDeviceTests.swift, Test/Utils/MPLaunchInfoTests.m, + Test/Utils/MPNetworkCommunicationHelperTests.swift, Test/Utils/MPResponseConfigTests.swift, Test/Utils/MPUserAttributeChangeTests.m, Test/Utils/MPUserDefaultsTests.swift, Test/Utils/MPZipTests.m, "Test/Utils/NSArray+MPCaseInsensitiveTests.swift", + "Test/Utils/NSDictionary+MPRetryAfterTests.swift", "Test/Utils/NSNumber+MPFormatterTests.m", "Test/Utils/NSString+MPPercentEscapeTests.swift", ); @@ -1178,6 +1184,7 @@ 35E3FCC22E53B5C200DB5B18 /* MPAttributionResult+MParticlePrivateTests.swift */, 72FEBD162E86FE2D00B8341F /* MPIdentityTests.swift */, 72D356522E8460020012A0C2 /* MPEventTests.swift */, + 35A1B2C22F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift */, ); path = SwiftTests; sourceTree = ""; @@ -1562,6 +1569,7 @@ 534CD28D29CE2CE1008452B3 /* MPUploadBuilderTests.m in Sources */, 534CD28E29CE2CE1008452B3 /* MPBaseTestCase.m in Sources */, 35329FEC2E54C483009AC4FD /* MPNetworkOptions+MParticlePrivateTests.swift in Sources */, + 35A1B2C32F50011100A1B2C3 /* MPTransportErrorDetectorTests.swift in Sources */, 7231B8352EB95F9F001565E5 /* MParticleBreadcrumbTests.swift in Sources */, 534CD28F29CE2CE1008452B3 /* MPIdentityApiRequestTests.m in Sources */, 534CD29029CE2CE1008452B3 /* MPKitAppsFlyerTest.m in Sources */, diff --git a/mParticle-Apple-SDK/Network/MPConnector.m b/mParticle-Apple-SDK/Network/MPConnector.m index 07f54054c..d6e168b11 100644 --- a/mParticle-Apple-SDK/Network/MPConnector.m +++ b/mParticle-Apple-SDK/Network/MPConnector.m @@ -6,6 +6,12 @@ #import "MPILogger.h" #import "mParticle.h" #import "MPURL.h" +@import mParticle_Apple_SDK_Swift; + +@interface MPTransportErrorDetector (MPConnectorTimeout) ++ (NSString *)semaphoreTimeoutErrorDomain; ++ (NSNumber *)semaphoreTimeoutErrorCode; +@end static NSArray *mpStoredCertificates = nil; @@ -273,7 +279,9 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp (long)completionHttpResponse.statusCode, (unsigned long)completionData.length); } else { MPILogError(@"GET request timed out after %ld seconds - host: %@", (long)(NETWORK_REQUEST_MAX_WAIT_SECONDS + 1), url.url.host); - response.error = [NSError errorWithDomain:@"com.mparticle" code:0 userInfo:@{@"mParticle Error":@"Semaphore wait timed out"}]; + response.error = [NSError errorWithDomain:[MPTransportErrorDetector semaphoreTimeoutErrorDomain] + code:[MPTransportErrorDetector semaphoreTimeoutErrorCode].integerValue + userInfo:@{@"mParticle Error":@"Semaphore wait timed out"}]; [_urlSession invalidateAndCancel]; } @@ -318,7 +326,9 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp (long)completionHttpResponse.statusCode, (unsigned long)completionData.length); } else { MPILogError(@"POST request timed out after %ld seconds - host: %@", (long)(NETWORK_REQUEST_MAX_WAIT_SECONDS + 1), url.url.host); - response.error = [NSError errorWithDomain:@"com.mparticle" code:0 userInfo:@{@"mParticle Error":@"Semaphore wait timed out"}]; + response.error = [NSError errorWithDomain:[MPTransportErrorDetector semaphoreTimeoutErrorDomain] + code:[MPTransportErrorDetector semaphoreTimeoutErrorCode].integerValue + userInfo:@{@"mParticle Error":@"Semaphore wait timed out"}]; [_urlSession invalidateAndCancel]; } } else { diff --git a/mParticle-Apple-SDK/Network/MPNetworkCommunication.m b/mParticle-Apple-SDK/Network/MPNetworkCommunication.m index c0ecbc97a..e89fe76f5 100644 --- a/mParticle-Apple-SDK/Network/MPNetworkCommunication.m +++ b/mParticle-Apple-SDK/Network/MPNetworkCommunication.m @@ -67,6 +67,7 @@ @interface MParticle () @property (nonatomic, strong, readonly) MPStateMachine_PRIVATE *stateMachine; @property (nonatomic, strong, readonly) MPBackendController_PRIVATE *backendController; +- (MPLog *)getLogger; - (void)logKitBatch:(NSString *)batch; + (void)executeOnMain:(void(^)(void))block; + (void)executeOnMainSync:(void(^)(void))block; @@ -435,43 +436,24 @@ - (void)setIdentifying:(BOOL)identifying { } #pragma mark Private methods -- (void)throttleWithHTTPResponse:(NSHTTPURLResponse *)httpResponse uploadType:(MPUploadType)uploadType { +- (BOOL)isRetriableTransportError:(NSError *)error { + return [MPTransportErrorDetector isRetriableTransportError:error]; +} + +- (void)throttleWithRetryAfter:(NSTimeInterval)retryAfter uploadType:(MPUploadType)uploadType { + MParticle* mparticle = MParticle.sharedInstance; + MPLog *logger = mparticle.getLogger; NSDate *now = [NSDate date]; - NSDictionary *httpHeaders = [httpResponse allHeaderFields]; - NSTimeInterval retryAfter = 7200; // Default of 2 hours - NSTimeInterval maxRetryAfter = 86400; // Maximum of 24 hours - id suggestedRetryAfter = httpHeaders[@"Retry-After"]; - - if (!MPIsNull(suggestedRetryAfter)) { - if ([suggestedRetryAfter isKindOfClass:[NSString class]]) { - if ([suggestedRetryAfter containsString:@":"]) { // Date - NSDate *retryAfterDate = [MPDateFormatter dateFromStringRFC1123:(NSString *)suggestedRetryAfter]; - if (retryAfterDate) { - retryAfter = MIN(([retryAfterDate timeIntervalSince1970] - [now timeIntervalSince1970]), maxRetryAfter); - retryAfter = retryAfter > 0 ? retryAfter : 7200; - } else { - MPILogError(@"Invalid 'Retry-After' date: %@ - using default retry interval", suggestedRetryAfter); - } - } else { // Number of seconds - @try { - retryAfter = MIN([(NSString *)suggestedRetryAfter doubleValue], maxRetryAfter); - } @catch (NSException *exception) { - retryAfter = 7200; - MPILogError(@"Invalid 'Retry-After' value: %@ - using default retry interval", suggestedRetryAfter); - } - } - } else if ([suggestedRetryAfter isKindOfClass:[NSNumber class]]) { - retryAfter = MIN([(NSNumber *)suggestedRetryAfter doubleValue], maxRetryAfter); - } - } NSDate *minUploadDate = [MParticle.sharedInstance.stateMachine minUploadDateForUploadType:uploadType]; if ([minUploadDate compare:now] == NSOrderedAscending) { - [MParticle.sharedInstance.stateMachine setMinUploadDate:[now dateByAddingTimeInterval:retryAfter] uploadType:uploadType]; + [mparticle.stateMachine setMinUploadDate:[now dateByAddingTimeInterval:retryAfter] uploadType:uploadType]; if (uploadType == MPUploadTypeMessage) { - MPILogDebug(@"Throttling uploads for %.0f seconds", retryAfter); + NSString *messageThrottleLog = [NSString stringWithFormat:@"Throttling uploads for %.0f seconds", retryAfter]; + [logger debug:messageThrottleLog]; } else if (uploadType == MPUploadTypeAlias) { - MPILogDebug(@"Throttling alias requests for %.0f seconds", retryAfter); + NSString *aliasThrottleLog = [NSString stringWithFormat:@"Throttling alias requests for %.0f seconds", retryAfter]; + [logger debug:aliasThrottleLog]; } } } @@ -681,7 +663,11 @@ - (void)requestAudiencesWithCompletionHandler:(MPAudienceResponseHandler)complet } - (BOOL)performMessageUpload:(MPUpload *)upload { - NSDate *minUploadDate = [MParticle.sharedInstance.stateMachine minUploadDateForUploadType:MPUploadTypeMessage]; + MParticle *mParticle = MParticle.sharedInstance; + MPStateMachine_PRIVATE *stateMachine = mParticle.stateMachine; + MPPersistenceController_PRIVATE *persistenceController = mParticle.persistenceController; + + NSDate *minUploadDate = [stateMachine minUploadDateForUploadType:MPUploadTypeMessage]; if ([minUploadDate compare:[NSDate date]] == NSOrderedDescending) { return YES; //stop upload loop } @@ -694,8 +680,8 @@ - (BOOL)performMessageUpload:(MPUpload *)upload { MPILogVerbose(@"Beginning upload for upload ID: %@", upload.uuid); NSData *zipUploadData; - NSNumber *authTimestamp = [MParticle sharedInstance].stateMachine.attAuthorizationTimestamp; - NSNumber *authStatus = [MParticle sharedInstance].stateMachine.attAuthorizationStatus; + NSNumber *authTimestamp = stateMachine.attAuthorizationTimestamp; + NSNumber *authStatus = stateMachine.attAuthorizationStatus; if (authStatus != nil && authTimestamp != nil) { NSDictionary *uploadDictionary = [NSJSONSerialization JSONObjectWithData:upload.uploadData options:0 error:nil]; @@ -736,7 +722,7 @@ - (BOOL)performMessageUpload:(MPUpload *)upload { } if (zipUploadData == nil || zipUploadData.length <= 0) { - [[MParticle sharedInstance].persistenceController deleteUpload:upload]; + [persistenceController deleteUpload:upload]; return NO; } NSTimeInterval start = [[NSDate date] timeIntervalSince1970]; @@ -746,16 +732,20 @@ - (BOOL)performMessageUpload:(MPUpload *)upload { serializedParams:zipUploadData secret:upload.uploadSettings.secret]; NSData *data = response.data; + NSError *error = response.error; NSHTTPURLResponse *httpResponse = response.httpResponse; NSInteger responseCode = [httpResponse statusCode]; MPILogVerbose(@"Upload response code: %ld", (long)responseCode); BOOL isSuccessCode = responseCode >= 200 && responseCode < 300; BOOL isInvalidCode = responseCode != 429 && responseCode >= 400 && responseCode < 500; + if (isSuccessCode) { + [MPTransportErrorDetector resetTransportErrorCounter]; + } if (isSuccessCode || isInvalidCode) { - [[MParticle sharedInstance].persistenceController deleteUpload:upload]; + [persistenceController deleteUpload:upload]; if (isSuccessCode && uploadString.length) { - [[MParticle sharedInstance] logKitBatch:uploadString]; + [mParticle logKitBatch:uploadString]; } } @@ -780,12 +770,19 @@ - (BOOL)performMessageUpload:(MPUpload *)upload { // 429, 503 if (responseCode == HTTPStatusCodeServiceUnavailable || responseCode == HTTPStatusCodeTooManyRequests) { - [self throttleWithHTTPResponse:httpResponse uploadType:MPUploadTypeMessage]; + NSDictionary *httpHeaders = [httpResponse allHeaderFields]; + NSTimeInterval retryAfter = [[MPNetworkCommunicationHelper calculateRetryTimeForHeaders:httpHeaders] doubleValue]; + [self throttleWithRetryAfter:retryAfter uploadType:MPUploadTypeMessage]; return YES; } //5xx, 0, 999, -1, etc if (!isSuccessCode && !isInvalidCode) { + if ([self isRetriableTransportError:error]) { + MPILogWarning(@"Throttling uploads after transport error."); + NSTimeInterval retryAfter = [[MPTransportErrorDetector calculateRetryTimeForTransportError] doubleValue]; + [self throttleWithRetryAfter:retryAfter uploadType:MPUploadTypeMessage]; + } return YES; } @@ -818,6 +815,7 @@ - (BOOL)performAliasUpload:(MPUpload *)upload { serializedParams:upload.uploadData secret:upload.uploadSettings.secret]; NSData *data = response.data; + NSError *error = response.error; NSHTTPURLResponse *httpResponse = response.httpResponse; NSInteger responseCode = [httpResponse statusCode]; @@ -825,6 +823,9 @@ - (BOOL)performAliasUpload:(MPUpload *)upload { BOOL isSuccessCode = responseCode >= 200 && responseCode < 300; BOOL isInvalidCode = responseCode != 429 && responseCode >= 400 && responseCode < 500; + if (isSuccessCode) { + [MPTransportErrorDetector resetTransportErrorCounter]; + } if (isSuccessCode || isInvalidCode) { [[MParticle sharedInstance].persistenceController deleteUpload:upload]; } @@ -868,12 +869,19 @@ - (BOOL)performAliasUpload:(MPUpload *)upload { // 429, 503 if (responseCode == HTTPStatusCodeServiceUnavailable || responseCode == HTTPStatusCodeTooManyRequests) { aliasResponse.willRetry = YES; - [self throttleWithHTTPResponse:httpResponse uploadType:upload.uploadType]; + NSDictionary *httpHeaders = [httpResponse allHeaderFields]; + NSTimeInterval retryAfter = [[MPNetworkCommunicationHelper calculateRetryTimeForHeaders:httpHeaders] doubleValue]; + [self throttleWithRetryAfter:retryAfter uploadType:upload.uploadType]; return YES; } //5xx, 0, 999, -1, etc if (!isSuccessCode && !isInvalidCode) { + if ([self isRetriableTransportError:error]) { + MPILogWarning(@"Throttling alias requests after transport error."); + NSTimeInterval retryAfter = [[MPTransportErrorDetector calculateRetryTimeForTransportError] doubleValue]; + [self throttleWithRetryAfter:retryAfter uploadType:MPUploadTypeAlias]; + } return YES; }