Skip to content

Commit dad646b

Browse files
committed
Add a post-create delay to access new users / subscriptions
* This is a new behavior which waits a specific amount of time after something is created (such as a User or Subscription) before making a network call to get or update it. * The main motivation is that the OneSignal's server may return a 404 if you attempt to GET or PATCH on something that was just created. This is due fact that OneSignal's backend server replication sometimes has a delay under load. This may be considered a bug in the backend, but the SDK has a responsibility to handle this case as well. Additional Details: * User Manager owns an instance of OSNewRecordsState * Requests will check their records within the `prepareForExecution()` check
1 parent 9e7deb5 commit dad646b

19 files changed

+175
-72
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@
177177
3CEE93542B7C78EC008440BD /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
178178
3CEE93572B7C78FD008440BD /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; };
179179
3CEE93582B7C78FE008440BD /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
180+
3CF1A5632C669EA40056B3AA /* OSNewRecordsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */; };
180181
3CF8629E28A183F900776CA4 /* OSIdentityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */; };
181182
3CF862A028A1964F00776CA4 /* OSPropertiesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */; };
182183
3CF862A228A197D200776CA4 /* OSPropertiesModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */; };
@@ -1294,6 +1295,7 @@
12941295
3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModelStoreListener.swift; sourceTree = "<group>"; };
12951296
3CEE90A62BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesSupportedProperty.swift; sourceTree = "<group>"; };
12961297
3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneSignalRequest+UnitTests.swift"; sourceTree = "<group>"; };
1298+
3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSNewRecordsState.swift; sourceTree = "<group>"; };
12971299
3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = "<group>"; };
12981300
3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModel.swift; sourceTree = "<group>"; };
12991301
3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModelStoreListener.swift; sourceTree = "<group>"; };
@@ -2051,6 +2053,7 @@
20512053
3C115188289ADEA300565C41 /* OSModelStore.swift */,
20522054
3C115186289ADE7700565C41 /* OSModelStoreListener.swift */,
20532055
3C115184289ADE4F00565C41 /* OSModel.swift */,
2056+
3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */,
20542057
3C11518A289ADEEB00565C41 /* OSEventProducer.swift */,
20552058
3C11518C289AF5E800565C41 /* OSModelChangedHandler.swift */,
20562059
3C4F9E4328A4466C009F453A /* OSOperationRepo.swift */,
@@ -4038,6 +4041,7 @@
40384041
3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */,
40394042
3C115189289ADEA300565C41 /* OSModelStore.swift in Sources */,
40404043
3C115185289ADE4F00565C41 /* OSModel.swift in Sources */,
4044+
3CF1A5632C669EA40056B3AA /* OSNewRecordsState.swift in Sources */,
40414045
3C448BA22936B474002F96BC /* OSBackgroundTaskManager.swift in Sources */,
40424046
3C115187289ADE7700565C41 /* OSModelStoreListener.swift in Sources */,
40434047
3CE5F9E3289D88DC004A156E /* OSModelStoreChangedHandler.swift in Sources */,

iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,13 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
259259

260260
// Flush interval for operation repo in milliseconds
261261
#define POLL_INTERVAL_MS 5000
262+
263+
/**
264+
The number of seconds to delay after an operation completes that creates or changes IDs.
265+
This is a "cold down" period to avoid a caveat with OneSignal's backend replication, where you may
266+
incorrectlyget a 404 when attempting a GET or PATCH REST API call on something just after it is created.
267+
*/
268+
#define OP_REPO_POST_CREATE_DELAY_SECONDS 3
262269
#else
263270
// Test defines for API Client
264271
#define REATTEMPT_DELAY 0.004
@@ -279,6 +286,8 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
279286
// Reduce flush interval for operation repo in tests
280287
#define POLL_INTERVAL_MS 100
281288

289+
// Reduce delay in tests
290+
#define OP_REPO_POST_CREATE_DELAY_SECONDS 0
282291
#endif
283292

284293
// A max timeout for a request, which might include multiple reattempts
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
*/
27+
28+
import OneSignalCore
29+
30+
/**
31+
* Purpose: Keeps track of IDs that were just created on the backend.
32+
* This list gets used to delay network calls to ensure upcoming
33+
* requests are ready to be accepted by the backend.
34+
*/
35+
public class OSNewRecordsState {
36+
/**
37+
Params:
38+
- Key - a string ID such as onesignal ID or subscription ID
39+
- Value - a Date timestamp of when the ID was created
40+
*/
41+
private var records: [String: Date] = [:]
42+
private let lock = NSRecursiveLock()
43+
44+
public init() { }
45+
46+
/**
47+
Only add a new record with the current timestamp if overwriting is requested, or it is not already present
48+
*/
49+
public func add(_ key: String, _ overwrite: Bool = false) {
50+
lock.withLock {
51+
if overwrite || records[key] == nil {
52+
records[key] = Date()
53+
}
54+
}
55+
}
56+
57+
public func canAccess(_ key: String?) -> Bool {
58+
lock.withLock {
59+
guard let key = key,
60+
let timeLastMovedOrCreated = records[key]
61+
else {
62+
return true
63+
}
64+
65+
let minimumTime = timeLastMovedOrCreated.addingTimeInterval(TimeInterval(OP_REPO_POST_CREATE_DELAY_SECONDS))
66+
67+
return Date() >= minimumTime
68+
}
69+
}
70+
}

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
3434
// To simplify uncaching, we maintain separate request queues for each type
3535
var addRequestQueue: [OSRequestAddAliases] = []
3636
var removeRequestQueue: [OSRequestRemoveAlias] = []
37+
let newRecordsState: OSNewRecordsState
3738

3839
// The Identity executor dispatch queue, serial. This synchronizes access to the delta and request queues.
3940
private let dispatchQueue = DispatchQueue(label: "OneSignal.OSIdentityOperationExecutor", target: .global())
4041

41-
init() {
42+
init(newRecordsState: OSNewRecordsState) {
43+
self.newRecordsState = newRecordsState
4244
// Read unfinished deltas and requests from cache, if any...
4345
uncacheDeltas()
4446
uncacheAddAliasRequests()
@@ -72,7 +74,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
7274
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
7375
// 1. The model exists in the repo, so set it to be the Request's models
7476
request.identityModel = identityModel
75-
} else if request.prepareForExecution() {
77+
} else if request.prepareForExecution(newRecordsState: newRecordsState) {
7678
// 2. The request can be sent, add the model to the repo
7779
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
7880
} else {
@@ -95,7 +97,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
9597
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
9698
// 1. The model exists in the repo, so set it to be the Request's model
9799
request.identityModel = identityModel
98-
} else if request.prepareForExecution() {
100+
} else if request.prepareForExecution(newRecordsState: newRecordsState) {
99101
// 2. The request can be sent, add the model to the repo
100102
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
101103
} else {
@@ -191,7 +193,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
191193
guard !request.sentToClient else {
192194
return
193195
}
194-
guard request.prepareForExecution() else {
196+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
195197
return
196198
}
197199
request.sentToClient = true
@@ -250,7 +252,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
250252
guard !request.sentToClient else {
251253
return
252254
}
253-
guard request.prepareForExecution() else {
255+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
254256
return
255257
}
256258
request.sentToClient = true

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
6464
var supportedDeltas: [String] = [OS_UPDATE_PROPERTIES_DELTA]
6565
var deltaQueue: [OSDelta] = []
6666
var updateRequestQueue: [OSRequestUpdateProperties] = []
67+
let newRecordsState: OSNewRecordsState
6768

6869
// The property executor dispatch queue, serial. This synchronizes access to `deltaQueue` and `updateRequestQueue`.
6970
private let dispatchQueue = DispatchQueue(label: "OneSignal.OSPropertyOperationExecutor", target: .global())
7071

71-
init() {
72+
init(newRecordsState: OSNewRecordsState) {
73+
self.newRecordsState = newRecordsState
7274
// Read unfinished deltas and requests from cache, if any...
7375
// Note that we should only have deltas for the current user as old ones are flushed..
7476
uncacheDeltas()
@@ -98,7 +100,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
98100
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
99101
// 1. The identity model exist in the repo, set it to be the Request's model
100102
request.identityModel = identityModel
101-
} else if request.prepareForExecution() {
103+
} else if request.prepareForExecution(newRecordsState: newRecordsState) {
102104
// 2. The request can be sent, add the model to the repo
103105
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
104106
} else {
@@ -233,7 +235,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
233235
guard !request.sentToClient else {
234236
return
235237
}
236-
guard request.prepareForExecution() else {
238+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
237239
return
238240
}
239241
request.sentToClient = true

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
3636
var removeRequestQueue: [OSRequestDeleteSubscription] = []
3737
var updateRequestQueue: [OSRequestUpdateSubscription] = []
3838
var subscriptionModels: [String: OSSubscriptionModel] = [:]
39+
let newRecordsState: OSNewRecordsState
3940

4041
// The Subscription executor dispatch queue, serial. This synchronizes access to the delta and request queues.
4142
private let dispatchQueue = DispatchQueue(label: "OneSignal.OSSubscriptionOperationExecutor", target: .global())
4243

43-
init() {
44+
init(newRecordsState: OSNewRecordsState) {
45+
self.newRecordsState = newRecordsState
4446
// Read unfinished deltas and requests from cache, if any...
4547
uncacheDeltas()
4648
uncacheCreateSubscriptionRequests()
@@ -89,7 +91,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
8991
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
9092
// a. The model exist in the repo
9193
request.identityModel = identityModel
92-
} else if request.prepareForExecution() {
94+
} else if request.prepareForExecution(newRecordsState: newRecordsState) {
9395
// b. The request can be sent, add the model to the repo
9496
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
9597
} else {
@@ -116,7 +118,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
116118
} else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] {
117119
// 2. The model exists in the dict of seen subscription models
118120
request.subscriptionModel = subscriptionModel
119-
} else if !request.prepareForExecution() {
121+
} else if !request.prepareForExecution(newRecordsState: newRecordsState) {
120122
// 3. The model does not exist AND this request cannot be sent, drop this Request
121123
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.init dropped \(request)")
122124
removeRequestQueue.remove(at: index)
@@ -139,7 +141,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
139141
} else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] {
140142
// 2. The model exists in the dict of seen subscription models
141143
request.subscriptionModel = subscriptionModel
142-
} else if !request.prepareForExecution() {
144+
} else if !request.prepareForExecution(newRecordsState: newRecordsState) {
143145
// 3. The models do not exist AND this request cannot be sent, drop this Request
144146
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.init dropped \(request)")
145147
updateRequestQueue.remove(at: index)
@@ -271,7 +273,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
271273
guard !request.sentToClient else {
272274
return
273275
}
274-
guard request.prepareForExecution() else {
276+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
275277
return
276278
}
277279
request.sentToClient = true
@@ -336,7 +338,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
336338
guard !request.sentToClient else {
337339
return
338340
}
339-
guard request.prepareForExecution() else {
341+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
340342
return
341343
}
342344
request.sentToClient = true
@@ -381,7 +383,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
381383
guard !request.sentToClient else {
382384
return
383385
}
384-
guard request.prepareForExecution() else {
386+
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
385387
return
386388
}
387389
request.sentToClient = true

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
120120

121121
var identityModelRepo = OSIdentityModelRepo()
122122

123+
let newRecordsState = OSNewRecordsState()
124+
123125
var hasCalledStart = false
124126

125127
private var jwtExpiredHandler: OSJwtExpiredHandler?
@@ -222,13 +224,13 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
222224

223225
// Setup the executors
224226
// The OSUserExecutor has to run first, before other executors
225-
self.userExecutor = OSUserExecutor()
227+
self.userExecutor = OSUserExecutor(newRecordsState: newRecordsState)
226228
OSOperationRepo.sharedInstance.start()
227229

228230
// Cannot initialize these executors in `init` as they reference the sharedInstance
229-
let propertyExecutor = OSPropertyOperationExecutor()
230-
let identityExecutor = OSIdentityOperationExecutor()
231-
let subscriptionExecutor = OSSubscriptionOperationExecutor()
231+
let propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState)
232+
let identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState)
233+
let subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState)
232234
self.propertyExecutor = propertyExecutor
233235
self.identityExecutor = identityExecutor
234236
self.subscriptionExecutor = subscriptionExecutor

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
*/
2727

2828
import OneSignalCore
29+
import OneSignalOSCore
2930

3031
class OSRequestAddAliases: OneSignalRequest, OSUserRequest {
3132
var sentToClient = false
@@ -37,15 +38,16 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest {
3738
var identityModel: OSIdentityModel
3839
let aliases: [String: String]
3940

40-
// requires a `onesignal_id` to send this request
41-
func prepareForExecution() -> Bool {
42-
if let onesignalId = identityModel.onesignalId, let appId = OneSignalConfigManager.getAppId() {
41+
/// requires a `onesignal_id` to send this request
42+
func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool {
43+
if let onesignalId = identityModel.onesignalId,
44+
newRecordsState.canAccess(onesignalId),
45+
let appId = OneSignalConfigManager.getAppId()
46+
{
4347
self.addJWTHeader(identityModel: identityModel)
4448
self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/identity"
4549
return true
4650
} else {
47-
// self.path is non-nil, so set to empty string
48-
self.path = ""
4951
return false
5052
}
5153
}
@@ -57,7 +59,6 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest {
5759
super.init()
5860
self.parameters = ["identity": aliases]
5961
self.method = PATCH
60-
_ = prepareForExecution() // sets the path property
6162
}
6263

6364
func encode(with coder: NSCoder) {
@@ -86,6 +87,5 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest {
8687
self.parameters = parameters
8788
self.method = HTTPMethod(rawValue: rawMethod)
8889
self.timestamp = timestamp
89-
_ = prepareForExecution()
9090
}
9191
}

0 commit comments

Comments
 (0)