Skip to content

Commit 9ac1053

Browse files
author
Rodrigo Gomez Palacio
committed
OSMessagingController Retry Logic
Motivation: retry logic should work as follows: 1. initial request with offset --> failure 2. get retry limit & retry after from response & start retrying 3. if hit limit, make one final request with the offset: 0 The final request tells the backend, just show me what you got. Example: If the retry limit is 3 & we never get a successful response retrying, we should end up with 5 total requests.
1 parent e7d65f6 commit 9ac1053

File tree

4 files changed

+236
-46
lines changed

4 files changed

+236
-46
lines changed

iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m

Lines changed: 148 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Modified MIT License
33
*
4-
* Copyright 2017 OneSignal
4+
* Copyright 2024 OneSignal
55
*
66
* Permission is hereby granted, free of charge, to any person obtaining a copy
77
* of this software and associated documentation files (the "Software"), to deal
@@ -35,8 +35,14 @@
3535
#import "OSInAppMessagePrompt.h"
3636
#import "OSInAppMessagingRequests.h"
3737
#import "OneSignalWebViewManager.h"
38+
#import "OneSignalTracker.h"
3839
#import <OneSignalOutcomes/OneSignalOutcomes.h>
3940
#import "OSSessionManager.h"
41+
#import "OneSignalOSCore/OneSignalOSCore-Swift.h"
42+
43+
static NSInteger const DEFAULT_RETRY_AFTER_SECONDS = 1; // Default 1 second retry delay
44+
static NSInteger const DEFAULT_RETRY_LIMIT = 0; // If not returned by backend, don't retry
45+
static NSInteger const IAM_FETCH_DELAY_BUFFER = 0.5; // Fallback value if ryw_delay is nil: delay by 500 ms to increase the probability of getting a 200 & not having to retry
4046

4147
@implementation OSInAppMessageWillDisplayEvent
4248

@@ -242,22 +248,70 @@ - (void)updateInAppMessagesFromCache {
242248
}
243249

244250
- (void)getInAppMessagesFromServer:(NSString *)subscriptionId {
245-
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer"];
251+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
252+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer"];
246253

247-
if (!subscriptionId) {
248-
[self updateInAppMessagesFromCache];
249-
return;
250-
}
251-
252-
OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId];
253-
[OneSignalCoreImpl.sharedClient executeRequest:request onSuccess:^(NSDictionary *result) {
254+
if (!subscriptionId) {
255+
[self updateInAppMessagesFromCache];
256+
return;
257+
}
258+
259+
OSConsistencyManager *consistencyManager = [OSConsistencyManager shared];
260+
NSString *onesignalId = OneSignalUserManagerImpl.sharedInstance.onesignalId;
261+
262+
if (!onesignalId) {
263+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Failed to get in app messages due to no OneSignal ID"];
264+
return;
265+
}
266+
267+
OSIamFetchReadyCondition *condition = [OSIamFetchReadyCondition sharedInstanceWithId:onesignalId];
268+
OSReadYourWriteData *rywData = [consistencyManager getRywTokenFromAwaitableCondition:condition forId:onesignalId];
269+
270+
// We need to delay the first request by however long the backend is telling us (`ryw_delay`)
271+
// This will help avoid unnecessary retries & can be easily adjusted from the backend
272+
NSTimeInterval rywDelayInSeconds;
273+
if (rywData.rywDelay) {
274+
rywDelayInSeconds = [rywData.rywDelay doubleValue] / 1000.0;
275+
} else {
276+
rywDelayInSeconds = IAM_FETCH_DELAY_BUFFER;
277+
}
278+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rywDelayInSeconds * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
279+
280+
// Initial request
281+
[self attemptFetchWithRetries:subscriptionId
282+
rywData:rywData
283+
attempts:@0 // Starting with 0 attempts
284+
retryLimit:nil]; // Retry limit to be set dynamically on first failure
285+
});
286+
});
287+
}
288+
289+
290+
- (void)attemptFetchWithRetries:(NSString *)subscriptionId
291+
rywData:(OSReadYourWriteData *)rywData
292+
attempts:(NSNumber *)attempts
293+
retryLimit:(NSNumber *)retryLimit {
294+
NSNumber *sessionDuration = @([OSSessionManager.sharedSessionManager getTimeFocusedElapsed]);
295+
NSString *rywToken = rywData.rywToken;
296+
NSNumber *rywDelay = rywData.rywDelay;
297+
298+
// Create the request with the current attempt count
299+
OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId
300+
withSessionDuration:sessionDuration
301+
withRetryCount:attempts
302+
withRywToken:rywToken];
303+
304+
__block NSNumber *blockRetryLimit = retryLimit;
305+
306+
[OneSignalCoreImpl.sharedClient executeRequest:request
307+
onSuccess:^(NSDictionary *result) {
254308
dispatch_async(dispatch_get_main_queue(), ^{
255309
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer success"];
256-
if (result[@"in_app_messages"]) { // when there are no IAMs, will this still be there?
257-
let messages = [NSMutableArray new];
310+
if (result[@"in_app_messages"]) {
311+
NSMutableArray *messages = [NSMutableArray new];
258312

259313
for (NSDictionary *messageJson in result[@"in_app_messages"]) {
260-
let message = [OSInAppMessageInternal instanceWithJson:messageJson];
314+
OSInAppMessageInternal *message = [OSInAppMessageInternal instanceWithJson:messageJson];
261315
if (message) {
262316
[messages addObject:message];
263317
}
@@ -266,11 +320,89 @@ - (void)getInAppMessagesFromServer:(NSString *)subscriptionId {
266320
[self updateInAppMessagesFromServer:messages];
267321
return;
268322
}
323+
});
324+
}
325+
onFailure:^(NSError *error) {
326+
NSDictionary *errorInfo = error.userInfo[@"returned"];
327+
NSNumber *statusCode = errorInfo[@"httpStatusCode"];
328+
NSDictionary* responseHeaders = errorInfo[@"headers"];
329+
330+
if (!statusCode) {
331+
[self updateInAppMessagesFromCache];
332+
return;
333+
}
334+
335+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"getInAppMessagesFromServer failure: %@", error.localizedDescription]];
336+
337+
NSInteger code = [statusCode integerValue];
338+
if (code == 425 || code == 429) { // 425 Too Early or 429 Too Many Requests
339+
NSInteger retryAfter = [responseHeaders[@"Retry-After"] integerValue] ?: DEFAULT_RETRY_AFTER_SECONDS;
269340

270-
// TODO: Check this request and response. If no IAMs returned, should we really get from cache?
271-
// This is the existing implementation but it could mean this user has no IAMs?
272-
273-
// Default is using cached IAMs in the messaging controller
341+
// Dynamically set the retry limit from the header, if not already set
342+
if (!blockRetryLimit) {
343+
blockRetryLimit = @([responseHeaders[@"OneSignal-Retry-Limit"] integerValue] ?: DEFAULT_RETRY_LIMIT);
344+
}
345+
346+
if ([attempts integerValue] < [blockRetryLimit integerValue]) {
347+
NSInteger nextAttempt = [attempts integerValue] + 1; // Increment attempts
348+
[self retryAfterDelay:retryAfter
349+
subscriptionId:subscriptionId
350+
rywData:rywData
351+
attempts:@(nextAttempt)
352+
retryLimit:blockRetryLimit];
353+
} else {
354+
// Final attempt without rywToken
355+
[self fetchInAppMessagesWithoutToken:subscriptionId];
356+
}
357+
} else if (code >= 500 && code <= 599) {
358+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Server error, skipping retries"];
359+
[self updateInAppMessagesFromCache];
360+
} else {
361+
[self updateInAppMessagesFromCache];
362+
}
363+
}];
364+
}
365+
366+
- (void)retryAfterDelay:(NSInteger)retryAfter
367+
subscriptionId:(NSString *)subscriptionId
368+
rywData:(OSReadYourWriteData *)rywData
369+
attempts:(NSNumber *)attempts
370+
retryLimit:(NSNumber *)retryLimit {
371+
372+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryAfter * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
373+
374+
[self attemptFetchWithRetries:subscriptionId
375+
rywData:rywData
376+
attempts:attempts
377+
retryLimit:retryLimit];
378+
});
379+
}
380+
381+
- (void)fetchInAppMessagesWithoutToken:(NSString *)subscriptionId {
382+
NSNumber *sessionDuration = @([OSSessionManager.sharedSessionManager getTimeFocusedElapsed]);
383+
384+
OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId
385+
withSessionDuration:sessionDuration
386+
withRetryCount:nil
387+
withRywToken:nil]; // No retries for the final attempt
388+
389+
[OneSignalCoreImpl.sharedClient executeRequest:request
390+
onSuccess:^(NSDictionary *result) {
391+
dispatch_async(dispatch_get_main_queue(), ^{
392+
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Final attempt without token success"];
393+
if (result[@"in_app_messages"]) {
394+
NSMutableArray *messages = [NSMutableArray new];
395+
396+
for (NSDictionary *messageJson in result[@"in_app_messages"]) {
397+
OSInAppMessageInternal *message = [OSInAppMessageInternal instanceWithJson:messageJson];
398+
if (message) {
399+
[messages addObject:message];
400+
}
401+
}
402+
403+
[self updateInAppMessagesFromServer:messages];
404+
return;
405+
}
274406
[self updateInAppMessagesFromCache];
275407
});
276408
} onFailure:^(NSError *error) {

iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSCondition.swift

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
1-
//
2-
// OSCondition.swift
3-
// OneSignalOSCore
4-
//
5-
// Created by Rodrigo Gomez-Palacio on 9/10/24.
6-
// Copyright © 2024 OneSignal. All rights reserved.
7-
//
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2024 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
827

928
import Foundation
1029

iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyKeyEnum.swift

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
1-
//
2-
// OSConsistencyKeyEnum.swift
3-
// OneSignalOSCore
4-
//
5-
// Created by Rodrigo Gomez-Palacio on 9/10/24.
6-
// Copyright © 2024 OneSignal. All rights reserved.
7-
//
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2024 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
827

928
import Foundation
1029

iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Consistency/OSConsistencyManager.swift

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
1-
//
2-
// OSConsistencyManager.swift
3-
// OneSignalOSCore
4-
//
5-
// Created by Rodrigo Gomez-Palacio on 9/10/24.
6-
// Copyright © 2024 OneSignal. All rights reserved.
7-
//
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2024 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
827

928
import Foundation
29+
import OneSignalCore
1030

1131
@objc public class OSConsistencyManager: NSObject {
1232
// Singleton instance
@@ -41,10 +61,10 @@ import Foundation
4161
@objc public func getRywTokenFromAwaitableCondition(_ condition: OSCondition, forId id: String) -> OSReadYourWriteData? {
4262
let semaphore = DispatchSemaphore(value: 0)
4363
queue.sync {
44-
if self.conditions[id] == nil {
45-
self.conditions[id] = []
64+
if self.indexedConditions[id] == nil {
65+
self.indexedConditions[id] = []
4666
}
47-
self.conditions[id]?.append((condition, semaphore))
67+
self.indexedConditions[id]?.append((condition, semaphore))
4868
self.checkConditionsAndComplete(forId: id)
4969
}
5070
semaphore.wait() // Block until the condition is met
@@ -55,33 +75,33 @@ import Foundation
5575

5676
// Method to resolve conditions by condition ID (e.g. OSIamFetchReadyCondition.ID)
5777
@objc public func resolveConditionsWithID(id: String) {
58-
guard let conditionList = conditions[id] else { return }
78+
guard let conditionList = indexedConditions[id] else { return }
5979
var completedConditions: [(OSCondition, DispatchSemaphore)] = []
6080
for (condition, semaphore) in conditionList {
6181
if (condition.conditionId == id) {
6282
semaphore.signal()
6383
completedConditions.append((condition, semaphore))
6484
}
6585
}
66-
conditions[id]?.removeAll { condition, semaphore in
86+
indexedConditions[id]?.removeAll { condition, semaphore in
6787
completedConditions.contains(where: { $0.0 === condition && $0.1 == semaphore })
6888
}
6989
}
7090

7191
// Private method to check conditions for a specific id (unique ID like onesignalId)
7292
private func checkConditionsAndComplete(forId id: String) {
73-
guard let conditionList = conditions[id] else { return }
93+
guard let conditionList = indexedConditions[id] else { return }
7494
var completedConditions: [(OSCondition, DispatchSemaphore)] = []
7595
for (condition, semaphore) in conditionList {
7696
if condition.isMet(indexedTokens: indexedTokens) {
77-
print("Condition met for id: \(id)")
97+
OneSignalLog.onesignalLog(.LL_INFO, message: "Condition met for id: \(id)")
7898
semaphore.signal()
7999
completedConditions.append((condition, semaphore))
80100
} else {
81-
print("Condition not met for id: \(id)")
101+
OneSignalLog.onesignalLog(.LL_INFO, message: "Condition not met for id: \(id)")
82102
}
83103
}
84-
conditions[id]?.removeAll { condition, semaphore in
104+
indexedConditions[id]?.removeAll { condition, semaphore in
85105
completedConditions.contains(where: { $0.0 === condition && $0.1 == semaphore })
86106
}
87107
}

0 commit comments

Comments
 (0)