From dfa32ee4a9e5617ea53f514cb8e1dececcb53c1c Mon Sep 17 00:00:00 2001 From: Satheesh Kannan Date: Mon, 29 Jul 2024 12:58:08 +0530 Subject: [PATCH] feat: implement exponential retry mechanism for handling network errors --- Rudder.xcodeproj/project.pbxproj | 16 +++++ .../Headers/Public/RSExponentialBackOff.h | 40 +++++++++++++ Sources/Classes/Headers/Public/RSUtils.h | 1 + Sources/Classes/RSCloudModeManager.m | 21 ++++--- Sources/Classes/RSExponentialBackOff.m | 60 +++++++++++++++++++ Sources/Classes/RSUtils.m | 8 +++ 6 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 Sources/Classes/Headers/Public/RSExponentialBackOff.h create mode 100644 Sources/Classes/RSExponentialBackOff.m diff --git a/Rudder.xcodeproj/project.pbxproj b/Rudder.xcodeproj/project.pbxproj index b6714eee..50068218 100644 --- a/Rudder.xcodeproj/project.pbxproj +++ b/Rudder.xcodeproj/project.pbxproj @@ -10,6 +10,12 @@ 06CABC332630C6B00097BEFF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06CABC322630C6B00097BEFF /* Foundation.framework */; }; 06CABC352630C6D30097BEFF /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06CABC2E2630C6660097BEFF /* UIKit.framework */; }; 2FA4A3E2DF0696E8D68B640A /* Pods_Rudder_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F631D98DB26309EFE5E0A24 /* Pods_Rudder_watchOS.framework */; }; + 535778972C4F8E0000E8CE9B /* RSExponentialBackOff.m in Sources */ = {isa = PBXBuildFile; fileRef = 535778962C4F8E0000E8CE9B /* RSExponentialBackOff.m */; }; + 535778982C4F8E0000E8CE9B /* RSExponentialBackOff.h in Headers */ = {isa = PBXBuildFile; fileRef = 535778952C4F8E0000E8CE9B /* RSExponentialBackOff.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 535778992C4F8E0000E8CE9B /* RSExponentialBackOff.h in Headers */ = {isa = PBXBuildFile; fileRef = 535778952C4F8E0000E8CE9B /* RSExponentialBackOff.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5357789A2C4F8E0000E8CE9B /* RSExponentialBackOff.m in Sources */ = {isa = PBXBuildFile; fileRef = 535778962C4F8E0000E8CE9B /* RSExponentialBackOff.m */; }; + 5357789B2C4F8E0000E8CE9B /* RSExponentialBackOff.h in Headers */ = {isa = PBXBuildFile; fileRef = 535778952C4F8E0000E8CE9B /* RSExponentialBackOff.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5357789C2C4F8E0000E8CE9B /* RSExponentialBackOff.m in Sources */ = {isa = PBXBuildFile; fileRef = 535778962C4F8E0000E8CE9B /* RSExponentialBackOff.m */; }; 5C11F10C96D80DF73DBED732 /* Pods_Rudder_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 447958A767B9B6F9BB3AC36A /* Pods_Rudder_iOS.framework */; }; 60DF93A1FA405B16F4D273AB /* Pods_RudderTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F00C6B89DB926A1271D979A /* Pods_RudderTests_iOS.framework */; }; 7275AF62B3887AFD09CF49CF /* Pods_RudderTests_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E0F163A9A54245F87BB53E /* Pods_RudderTests_watchOS.framework */; }; @@ -762,6 +768,8 @@ 3C44A9E8F4B06BD7D1881CC6 /* Pods-RudderTests-watchOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RudderTests-watchOS.debug.xcconfig"; path = "Target Support Files/Pods-RudderTests-watchOS/Pods-RudderTests-watchOS.debug.xcconfig"; sourceTree = ""; }; 3F00C6B89DB926A1271D979A /* Pods_RudderTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RudderTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 447958A767B9B6F9BB3AC36A /* Pods_Rudder_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Rudder_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 535778952C4F8E0000E8CE9B /* RSExponentialBackOff.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RSExponentialBackOff.h; sourceTree = ""; }; + 535778962C4F8E0000E8CE9B /* RSExponentialBackOff.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RSExponentialBackOff.m; sourceTree = ""; }; 767A0EEE9A76F43CF631573B /* Pods-Rudder-watchOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Rudder-watchOS.debug.xcconfig"; path = "Target Support Files/Pods-Rudder-watchOS/Pods-Rudder-watchOS.debug.xcconfig"; sourceTree = ""; }; 83C9949585CD3C274CCE6A27 /* Pods-Rudder-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Rudder-iOS.release.xcconfig"; path = "Target Support Files/Pods-Rudder-iOS/Pods-Rudder-iOS.release.xcconfig"; sourceTree = ""; }; 9B4BB7D91351507AD413A9B4 /* Pods-Rudder-tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Rudder-tvOS.release.xcconfig"; path = "Target Support Files/Pods-Rudder-tvOS/Pods-Rudder-tvOS.release.xcconfig"; sourceTree = ""; }; @@ -1198,6 +1206,7 @@ F6B4B53529C8237D00344864 /* RSBackGroundModeManager.m */, ED7DFD8F298C091800ED5A8E /* RSClient.m */, F6B4B53929C8237D00344864 /* RSCloudModeManager.m */, + 535778962C4F8E0000E8CE9B /* RSExponentialBackOff.m */, ED7DFD89298C091800ED5A8E /* RSConfig.m */, ED7DFDCE298C091800ED5A8E /* RSConfigBuilder.m */, ED8DB01D298E206900907EC4 /* RSConsentFilterHandler.m */, @@ -1261,6 +1270,7 @@ F6B4B51E29C8236100344864 /* RSBackGroundModeManager.h */, ED7DFDB9298C091800ED5A8E /* RSClient.h */, F6B4B51D29C8236100344864 /* RSCloudModeManager.h */, + 535778952C4F8E0000E8CE9B /* RSExponentialBackOff.h */, ED7DFDB4298C091800ED5A8E /* RSConfig.h */, ED7DFDA5298C091800ED5A8E /* RSConfigBuilder.h */, ED7DFDAD298C091800ED5A8E /* RSConsentFilter.h */, @@ -1555,6 +1565,7 @@ ED7DFE70298C091800ED5A8E /* RSServerDestination.h in Headers */, ED7DFED9298C091800ED5A8E /* RSProductClickedEvent.h in Headers */, ED7DFE82298C091800ED5A8E /* RSECommerceCartBuilder.h in Headers */, + 535778982C4F8E0000E8CE9B /* RSExponentialBackOff.h in Headers */, ED99908E2A6926E000031B06 /* RSMetricsReporter.h in Headers */, ED7DFE58298C091800ED5A8E /* RSPreferenceManager.h in Headers */, ED7DFEB7298C091800ED5A8E /* RSPaymentInfoEnteredEvent.h in Headers */, @@ -1673,6 +1684,7 @@ ED998E682A69003600031B06 /* RSECommerceCartBuilder.h in Headers */, ED99908F2A6926E000031B06 /* RSMetricsReporter.h in Headers */, ED998E692A69003600031B06 /* RSPreferenceManager.h in Headers */, + 535778992C4F8E0000E8CE9B /* RSExponentialBackOff.h in Headers */, ED998E6A2A69003600031B06 /* RSPaymentInfoEnteredEvent.h in Headers */, ED998E6B2A69003600031B06 /* RSCartSharedEvent.h in Headers */, ED998E6C2A69003600031B06 /* RSECommerceEvents.h in Headers */, @@ -1791,6 +1803,7 @@ ED998F382A69024E00031B06 /* RSECommerceCartBuilder.h in Headers */, ED9990902A6926E000031B06 /* RSMetricsReporter.h in Headers */, ED998F392A69024E00031B06 /* RSPreferenceManager.h in Headers */, + 5357789B2C4F8E0000E8CE9B /* RSExponentialBackOff.h in Headers */, ED998F3A2A69024E00031B06 /* RSPaymentInfoEnteredEvent.h in Headers */, ED998F3B2A69024E00031B06 /* RSCartSharedEvent.h in Headers */, ED998F3C2A69024E00031B06 /* RSECommerceEvents.h in Headers */, @@ -2378,6 +2391,7 @@ ED7DFE8F298C091800ED5A8E /* RSECommerceSortBuilder.m in Sources */, ED7DFE3F298C091800ED5A8E /* RSServerDestination.m in Sources */, ED7DFEB6298C091800ED5A8E /* RSOrderRefundedEvent.m in Sources */, + 535778972C4F8E0000E8CE9B /* RSExponentialBackOff.m in Sources */, F64116022B68EC4B0015CB42 /* RSDefaultsPersistence.m in Sources */, ED7DFEC2298C091800ED5A8E /* RSCouponEnteredEvent.m in Sources */, ED7DFE3E298C091800ED5A8E /* RSApp.m in Sources */, @@ -2488,6 +2502,7 @@ ED998EEF2A69003600031B06 /* WKInterfaceController+RSScreen.m in Sources */, ED998EF02A69003600031B06 /* RSECommerceSortBuilder.m in Sources */, ED998EF12A69003600031B06 /* RSServerDestination.m in Sources */, + 5357789A2C4F8E0000E8CE9B /* RSExponentialBackOff.m in Sources */, F64116032B68EC4B0015CB42 /* RSDefaultsPersistence.m in Sources */, ED998EF22A69003600031B06 /* RSOrderRefundedEvent.m in Sources */, ED998EF32A69003600031B06 /* RSCouponEnteredEvent.m in Sources */, @@ -2598,6 +2613,7 @@ ED998FBF2A69024E00031B06 /* WKInterfaceController+RSScreen.m in Sources */, ED998FC02A69024E00031B06 /* RSECommerceSortBuilder.m in Sources */, ED998FC12A69024E00031B06 /* RSServerDestination.m in Sources */, + 5357789C2C4F8E0000E8CE9B /* RSExponentialBackOff.m in Sources */, F64116042B68EC4B0015CB42 /* RSDefaultsPersistence.m in Sources */, ED998FC22A69024E00031B06 /* RSOrderRefundedEvent.m in Sources */, ED998FC32A69024E00031B06 /* RSCouponEnteredEvent.m in Sources */, diff --git a/Sources/Classes/Headers/Public/RSExponentialBackOff.h b/Sources/Classes/Headers/Public/RSExponentialBackOff.h new file mode 100644 index 00000000..61062115 --- /dev/null +++ b/Sources/Classes/Headers/Public/RSExponentialBackOff.h @@ -0,0 +1,40 @@ +// +// RSExponentialBackOff.h +// Rudder +// +// Created by Satheesh Kannan on 23/07/24. +// + +#import + +NS_ASSUME_NONNULL_BEGIN +/*! + @brief This class implements an exponential backoff strategy with jitter for handling retries. + + @discussion It allows for configurable maximum delay and includes methods to calculate the next delay with jitter and reset the backoff attempts. When the calculated delay reaches or exceeds the maximum delay limit, the backoff resets and starts again from beginning. + */ +@interface RSExponentialBackOff : NSObject + +/** + * Init function that accepts the maximum delay value in seconds. + * + * @param seconds Value for maximum delay property + * @return A new instance for this class + */ +- (instancetype)initWithMaximumDelay:(int)seconds; + +/** + * Function will calculate the next delay value in seconds + * + * @return Next delay value in seconds + */ +- (NSInteger)nextDelay; + +/** + * Function will resets the attempts. + */ +- (void)reset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Classes/Headers/Public/RSUtils.h b/Sources/Classes/Headers/Public/RSUtils.h index d2ef1c73..958e95c5 100644 --- a/Sources/Classes/Headers/Public/RSUtils.h +++ b/Sources/Classes/Headers/Public/RSUtils.h @@ -50,6 +50,7 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL) isValidIDFA:(NSString*)idfa; + (BOOL) isSpecialFloatingNumber:(NSNumber *)number; +(NSArray*) extractParamFromURL: (NSURL*) deepLinkURL; ++ (NSString *)delayToString:(int) delay; extern unsigned int MAX_EVENT_SIZE; extern unsigned int MAX_BATCH_SIZE; diff --git a/Sources/Classes/RSCloudModeManager.m b/Sources/Classes/RSCloudModeManager.m index 0072f065..a9e0471c 100644 --- a/Sources/Classes/RSCloudModeManager.m +++ b/Sources/Classes/RSCloudModeManager.m @@ -12,9 +12,11 @@ #import "RSNetworkManager.h" #import "RSNetworkResponse.h" #import "RSMetricsReporter.h" +#import "RSExponentialBackOff.h" -@implementation RSCloudModeManager - +@implementation RSCloudModeManager { + RSExponentialBackOff *backOff; +} - (instancetype)initWithConfig:(RSConfig *) config andDBPersistentManager:(RSDBPersistentManager *) dbPersistentManager andNetworkManager:(RSNetworkManager *) networkManager andLock: (NSLock *) lock { self = [super init]; @@ -24,6 +26,7 @@ - (instancetype)initWithConfig:(RSConfig *) config andDBPersistentManager:(RSDBP self->config = config; self->lock = lock; self->cloud_mode_processor_queue = dispatch_queue_create("com.rudder.RSCloudModeManager", NULL); + self->backOff = [[RSExponentialBackOff alloc] initWithMaximumDelay:5 * 60]; } return self; } @@ -60,8 +63,9 @@ - (void) startCloudModeProcessor { [strongSelf->lock unlock]; [RSLogger logDebug:[[NSString alloc] initWithFormat:@"RSCloudModeManager: CloudModeProcessor: cloudModeSleepCount: %d", sleepCount]]; sleepCount += 1; + if(response == nil) { - usleep(1000000); + sleep(1); } else if (response.state == WRONG_WRITE_KEY) { [RSLogger logError:@"RSCloudModeManager: CloudModeProcessor: Wrong WriteKey. Aborting the Cloud Mode Processor"]; break; @@ -69,11 +73,14 @@ - (void) startCloudModeProcessor { [RSLogger logError:@"RSCloudModeManager: CloudModeProcessor: Invalid Data Plane URL. Aborting the Cloud Mode Processor"]; [RSMetricsReporter report:SDKMETRICS_CM_ATTEMPT_ABORT forMetricType:COUNT withProperties:@{SDKMETRICS_TYPE: SDKMETRICS_DATA_PLANE_URL_INVALID} andValue:1]; break; - } - else if (response.state == NETWORK_ERROR) { - [RSLogger logDebug:[[NSString alloc] initWithFormat:@"RSCloudModeManager: CloudModeProcessor: Retrying in: %d s", abs(sleepCount - strongSelf->config.sleepTimeout)]]; + } else if (response.state == NETWORK_ERROR) { + int delay = (int)[self->backOff nextDelay]; + [RSLogger logDebug:[[NSString alloc] initWithFormat:@"RSCloudModeManager: CloudModeProcessor: Retrying in: %@", [self delayToString:delay]]]; [RSMetricsReporter report:SDKMETRICS_CM_ATTEMPT_RETRY forMetricType:COUNT withProperties:nil andValue:1]; - usleep(abs(sleepCount - strongSelf->config.sleepTimeout) * 1000000); + sleep(delay); + } else { // To handle the status code RESOURCE_NOT_FOUND(404) & BAD_REQUEST(400) + [RSLogger logDebug:[[NSString alloc] initWithFormat:@"RSCloudModeManager: CloudModeProcessor: Retrying in: 1s"]]; + sleep(1); } } }); diff --git a/Sources/Classes/RSExponentialBackOff.m b/Sources/Classes/RSExponentialBackOff.m new file mode 100644 index 00000000..80bc569e --- /dev/null +++ b/Sources/Classes/RSExponentialBackOff.m @@ -0,0 +1,60 @@ +// +// RSExponentialBackOff.m +// Rudder +// +// Created by Satheesh Kannan on 23/07/24. +// + +#import "RSExponentialBackOff.h" + +#pragma mark - RSExponentialBackOff + +@interface RSExponentialBackOff() +@property (nonatomic, assign) NSInteger attempt; +@property (nonatomic, assign) NSInteger maximumDelay; +@property (nonatomic, assign) NSInteger initialDelay; +@end + +@implementation RSExponentialBackOff + +/** + * Init function that accepts the maximum delay value in seconds. + */ +- (instancetype)initWithMaximumDelay:(int) seconds { + self = [super init]; + if (self) { + _maximumDelay = seconds; + _attempt = 0; + _initialDelay = 3; + } + + return self; +} + +/** + * Function will calculate the next delay value in seconds + */ +- (NSInteger)nextDelay { + NSInteger delay = (NSInteger)pow(2, _attempt++); + NSInteger jitter = arc4random_uniform((uint32_t)(delay + 1)); + + NSInteger exponentialDelay = _initialDelay + delay + jitter; + exponentialDelay = MIN(exponentialDelay, _maximumDelay); + + if (exponentialDelay >= _maximumDelay) { + _attempt = 0; + } + + return exponentialDelay; +} + +/** + * Function will resets the attempts. + */ +- (void)reset { + _attempt = 0; +} + +@end + + diff --git a/Sources/Classes/RSUtils.m b/Sources/Classes/RSUtils.m index 927fb1c7..aec4c4cb 100644 --- a/Sources/Classes/RSUtils.m +++ b/Sources/Classes/RSUtils.m @@ -291,4 +291,12 @@ +(NSArray*) extractParamFromURL: (NSURL*) deepLinkURL{ unsigned int MAX_EVENT_SIZE = 32 * 1024; // 32 KB unsigned int MAX_BATCH_SIZE = 500 * 1024; // 500 KB ++ (NSString *)delayToString:(int) delay { + int min = delay / 60; + int sec = min > 0 ? (delay - (min * 60)) : delay; + NSString *finalString = min > 0 ? [NSString stringWithFormat:@"%d min, %d sec", min, sec] : [NSString stringWithFormat:@"%d sec", sec]; + NSLog(@"SK--->>%@", finalString); + return finalString; +} + @end