Skip to content

Commit 12e8359

Browse files
authored
Merge pull request #2059 from OneSignal/improve/handle_incorrect_404
[Fix]Handle incorrect 404 responses; add a delay after creates and retries on 404 of new ids
2 parents cd9830c + a9dfc09 commit 12e8359

31 files changed

+618
-33
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,32 @@ class ConfigModel : Model() {
141141
setLongProperty(::opRepoPostWakeDelay.name, value)
142142
}
143143

144+
/**
145+
* The number of milliseconds to delay after an operation completes
146+
* that creates or changes ids.
147+
* This is a "cold down" period to avoid a caveat with OneSignal's backend
148+
* replication, where you may incorrectly get a 404 when attempting a GET
149+
* or PATCH REST API call on something just after it is created.
150+
*/
151+
var opRepoPostCreateDelay: Long
152+
get() = getLongProperty(::opRepoPostCreateDelay.name) { 5_000 }
153+
set(value) {
154+
setLongProperty(::opRepoPostCreateDelay.name, value)
155+
}
156+
157+
/**
158+
* The number of milliseconds to retry operations for new models.
159+
* This is a fallback to opRepoPostCreateDelay, where it's delay may
160+
* not be enough. The server may be unusually overloaded so we will
161+
* retry these (back-off rules apply to all retries) as we only want
162+
* to re-create records as a last resort.
163+
*/
164+
var opRepoPostCreateRetryUpTo: Long
165+
get() = getLongProperty(::opRepoPostCreateRetryUpTo.name) { 60_000 }
166+
set(value) {
167+
setLongProperty(::opRepoPostCreateRetryUpTo.name, value)
168+
}
169+
144170
/**
145171
* The minimum number of milliseconds required to pass to allow the fetching of IAM to occur.
146172
*/

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/Operation.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ abstract class Operation(name: String) : Model() {
2020
this.name = name
2121
}
2222

23+
/**
24+
* This is a unique id that points to a record this operation will affect.
25+
* Example: If the operation is updating tags on a User this will be the onesignalId.
26+
*/
27+
abstract val applyToRecordId: String
28+
2329
/**
2430
* The key of this operation for when the starting operation has a [groupComparisonType]
2531
* of [GroupComparisonType.CREATE]

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.onesignal.core.internal.operations.impl
22

33
import com.onesignal.common.threading.WaiterWithValue
4-
import com.onesignal.common.threading.suspendifyOnThread
54
import com.onesignal.core.internal.config.ConfigModelStore
65
import com.onesignal.core.internal.operations.ExecutionResult
76
import com.onesignal.core.internal.operations.GroupComparisonType
@@ -12,7 +11,11 @@ import com.onesignal.core.internal.startup.IStartableService
1211
import com.onesignal.core.internal.time.ITime
1312
import com.onesignal.debug.LogLevel
1413
import com.onesignal.debug.internal.logging.Logging
14+
import com.onesignal.user.internal.operations.impl.states.NewRecordsState
15+
import kotlinx.coroutines.CoroutineScope
1516
import kotlinx.coroutines.delay
17+
import kotlinx.coroutines.launch
18+
import kotlinx.coroutines.newSingleThreadContext
1619
import kotlinx.coroutines.withTimeoutOrNull
1720
import java.util.UUID
1821
import kotlin.reflect.KClass
@@ -22,6 +25,7 @@ internal class OperationRepo(
2225
private val _operationModelStore: OperationModelStore,
2326
private val _configModelStore: ConfigModelStore,
2427
private val _time: ITime,
28+
private val _newRecordState: NewRecordsState,
2529
) : IOperationRepo, IStartableService {
2630
internal class OperationQueueItem(
2731
val operation: Operation,
@@ -34,10 +38,16 @@ internal class OperationRepo(
3438
}
3539
}
3640

41+
internal class LoopWaiterMessage(
42+
val force: Boolean,
43+
val previousWaitedTime: Long = 0,
44+
)
45+
3746
private val executorsMap: Map<String, IOperationExecutor>
3847
private val queue = mutableListOf<OperationQueueItem>()
39-
private val waiter = WaiterWithValue<Boolean>()
48+
private val waiter = WaiterWithValue<LoopWaiterMessage>()
4049
private var paused = false
50+
private var coroutineScope = CoroutineScope(newSingleThreadContext(name = "OpRepo"))
4151

4252
/** *** Buckets ***
4353
* Purpose: Bucketing is a pattern we are using to help save network
@@ -56,13 +66,11 @@ internal class OperationRepo(
5666
* network calls.
5767
*/
5868
private var enqueueIntoBucket = 0
59-
private val executeBucket: Int get() {
60-
return if (enqueueIntoBucket == 0) 0 else enqueueIntoBucket - 1
61-
}
69+
private val executeBucket get() =
70+
if (enqueueIntoBucket == 0) 0 else enqueueIntoBucket - 1
6271

6372
init {
6473
val executorsMap: MutableMap<String, IOperationExecutor> = mutableMapOf()
65-
6674
for (executor in executors) {
6775
for (operation in executor.operations) {
6876
executorsMap[operation] = executor
@@ -83,9 +91,7 @@ internal class OperationRepo(
8391

8492
override fun start() {
8593
paused = false
86-
suspendifyOnThread(name = "OpRepo") {
87-
processQueueForever()
88-
}
94+
coroutineScope.launch { processQueueForever() }
8995
}
9096

9197
override fun enqueue(
@@ -123,7 +129,7 @@ internal class OperationRepo(
123129
}
124130
}
125131

126-
waiter.wake(flush)
132+
waiter.wake(LoopWaiterMessage(flush, 0))
127133
}
128134

129135
/**
@@ -160,16 +166,16 @@ internal class OperationRepo(
160166
*/
161167
private suspend fun waitForNewOperationAndExecutionInterval() {
162168
// 1. Wait for an operation to be enqueued
163-
var force = waiter.waitForWake()
169+
var wakeMessage = waiter.waitForWake()
164170

165171
// 2. Wait at least the time defined in opRepoExecutionInterval
166172
// so operations can be grouped, unless one of them used
167173
// flush=true (AKA force)
168174
var lastTime = _time.currentTimeMillis
169-
var remainingTime = _configModelStore.model.opRepoExecutionInterval
170-
while (!force && remainingTime > 0) {
175+
var remainingTime = _configModelStore.model.opRepoExecutionInterval - wakeMessage.previousWaitedTime
176+
while (!wakeMessage.force && remainingTime > 0) {
171177
withTimeoutOrNull(remainingTime) {
172-
force = waiter.waitForWake()
178+
wakeMessage = waiter.waitForWake()
173179
}
174180
remainingTime -= _time.currentTimeMillis - lastTime
175181
lastTime = _time.currentTimeMillis
@@ -195,6 +201,14 @@ internal class OperationRepo(
195201
synchronized(queue) {
196202
queue.forEach { it.operation.translateIds(response.idTranslations) }
197203
}
204+
response.idTranslations.values.forEach { _newRecordState.add(it) }
205+
coroutineScope.launch {
206+
val waitTime = _configModelStore.model.opRepoPostCreateDelay
207+
delay(waitTime)
208+
synchronized(queue) {
209+
if (queue.isNotEmpty()) waiter.wake(LoopWaiterMessage(false, waitTime))
210+
}
211+
}
198212
}
199213

200214
when (response.result) {
@@ -278,7 +292,9 @@ internal class OperationRepo(
278292
return synchronized(queue) {
279293
val startingOp =
280294
queue.firstOrNull {
281-
it.operation.canStartExecute && it.bucket <= bucketFilter
295+
it.operation.canStartExecute &&
296+
_newRecordState.canAccess(it.operation.applyToRecordId) &&
297+
it.bucket <= bucketFilter
282298
}
283299

284300
if (startingOp != null) {

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.onesignal.user.internal.operations.impl.executors.UpdateUserOperation
2525
import com.onesignal.user.internal.operations.impl.listeners.IdentityModelStoreListener
2626
import com.onesignal.user.internal.operations.impl.listeners.PropertiesModelStoreListener
2727
import com.onesignal.user.internal.operations.impl.listeners.SubscriptionModelStoreListener
28+
import com.onesignal.user.internal.operations.impl.states.NewRecordsState
2829
import com.onesignal.user.internal.properties.PropertiesModelStore
2930
import com.onesignal.user.internal.service.UserRefreshService
3031
import com.onesignal.user.internal.subscriptions.ISubscriptionManager
@@ -68,5 +69,8 @@ internal class UserModule : IModule {
6869
builder.register<UserRefreshService>().provides<IStartableService>()
6970

7071
builder.register<RecoverFromDroppedLoginBug>().provides<IStartableService>()
72+
73+
// Shared state between Executors
74+
builder.register<NewRecordsState>().provides<NewRecordsState>()
7175
}
7276
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/CreateSubscriptionOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class CreateSubscriptionOperation() : Operation(SubscriptionOperationExecutor.CR
8686
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.Subscription.$subscriptionId"
8787
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
8888
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
89+
override val applyToRecordId: String get() = onesignalId
8990

9091
constructor(appId: String, onesignalId: String, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) : this() {
9192
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteAliasOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class DeleteAliasOperation() : Operation(IdentityOperationExecutor.DELETE_ALIAS)
4141
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.Alias.$label"
4242
override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE
4343
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
44+
override val applyToRecordId: String get() = onesignalId
4445

4546
constructor(appId: String, onesignalId: String, label: String) : this() {
4647
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteSubscriptionOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class DeleteSubscriptionOperation() : Operation(SubscriptionOperationExecutor.DE
4242
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.Subscription.$subscriptionId"
4343
override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE
4444
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) && !IDManager.isLocalId(onesignalId)
45+
override val applyToRecordId: String get() = subscriptionId
4546

4647
constructor(appId: String, onesignalId: String, subscriptionId: String) : this() {
4748
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteTagOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class DeleteTagOperation() : Operation(UpdateUserOperationExecutor.DELETE_TAG) {
4242
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId"
4343
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
4444
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
45+
override val applyToRecordId: String get() = onesignalId
4546

4647
constructor(appId: String, onesignalId: String, key: String) : this() {
4748
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserFromSubscriptionOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class LoginUserFromSubscriptionOperation() : Operation(LoginUserFromSubscription
4040
override val modifyComparisonKey: String get() = "$appId.Subscription.$subscriptionId.Login"
4141
override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE
4242
override val canStartExecute: Boolean = true
43+
override val applyToRecordId: String get() = subscriptionId
4344

4445
constructor(appId: String, onesignalId: String, subscriptionId: String) : this() {
4546
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
5656
override val modifyComparisonKey: String = ""
5757
override val groupComparisonType: GroupComparisonType = GroupComparisonType.CREATE
5858
override val canStartExecute: Boolean get() = existingOnesignalId == null || !IDManager.isLocalId(existingOnesignalId!!)
59+
override val applyToRecordId: String get() = existingOnesignalId ?: onesignalId
5960

6061
constructor(appId: String, onesignalId: String, externalId: String?, existingOneSignalId: String? = null) : this() {
6162
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/RefreshUserOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class RefreshUserOperation() : Operation(RefreshUserOperationExecutor.REFRESH_US
3333
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.Refresh"
3434
override val groupComparisonType: GroupComparisonType = GroupComparisonType.CREATE
3535
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
36+
override val applyToRecordId: String get() = onesignalId
3637

3738
constructor(appId: String, onesignalId: String) : this() {
3839
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetAliasOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class SetAliasOperation() : Operation(IdentityOperationExecutor.SET_ALIAS) {
5151
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.Identity.$label"
5252
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
5353
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
54+
override val applyToRecordId: String get() = onesignalId
5455

5556
constructor(appId: String, onesignalId: String, label: String, value: String) : this() {
5657
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetPropertyOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class SetPropertyOperation() : Operation(UpdateUserOperationExecutor.SET_PROPERT
5050
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId"
5151
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
5252
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
53+
override val applyToRecordId: String get() = onesignalId
5354

5455
constructor(appId: String, onesignalId: String, property: String, value: Any?) : this() {
5556
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetTagOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class SetTagOperation() : Operation(UpdateUserOperationExecutor.SET_TAG) {
5151
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId"
5252
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
5353
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
54+
override val applyToRecordId: String get() = onesignalId
5455

5556
constructor(appId: String, onesignalId: String, key: String, value: String) : this() {
5657
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackPurchaseOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class TrackPurchaseOperation() : Operation(UpdateUserOperationExecutor.TRACK_PUR
6363
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId"
6464
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
6565
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
66+
override val applyToRecordId: String get() = onesignalId
6667

6768
constructor(appId: String, onesignalId: String, treatNewAsExisting: Boolean, amountSpent: BigDecimal, purchases: List<PurchaseInfo>) : this() {
6869
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionEndOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class TrackSessionEndOperation() : Operation(UpdateUserOperationExecutor.TRACK_S
4141
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId"
4242
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
4343
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
44+
override val applyToRecordId: String get() = onesignalId
4445

4546
constructor(appId: String, onesignalId: String, sessionTime: Long) : this() {
4647
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackSessionStartOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class TrackSessionStartOperation() : Operation(UpdateUserOperationExecutor.TRACK
3232
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId"
3333
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
3434
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId)
35+
override val applyToRecordId: String get() = onesignalId
3536

3637
constructor(appId: String, onesignalId: String) : this() {
3738
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TransferSubscriptionOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class TransferSubscriptionOperation() : Operation(SubscriptionOperationExecutor.
4242
override val modifyComparisonKey: String get() = "$appId.Subscription.$subscriptionId.Transfer"
4343
override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE
4444
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) && !IDManager.isLocalId(subscriptionId)
45+
override val applyToRecordId: String get() = subscriptionId
4546

4647
constructor(appId: String, subscriptionId: String, onesignalId: String) : this() {
4748
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/UpdateSubscriptionOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class UpdateSubscriptionOperation() : Operation(SubscriptionOperationExecutor.UP
8585
override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.Subscription.$subscriptionId"
8686
override val groupComparisonType: GroupComparisonType = GroupComparisonType.ALTER
8787
override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) && !IDManager.isLocalId(onesignalId)
88+
override val applyToRecordId: String get() = subscriptionId
8889

8990
constructor(appId: String, onesignalId: String, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) : this() {
9091
this.appId = appId

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import com.onesignal.user.internal.builduser.IRebuildUserService
1414
import com.onesignal.user.internal.identity.IdentityModelStore
1515
import com.onesignal.user.internal.operations.DeleteAliasOperation
1616
import com.onesignal.user.internal.operations.SetAliasOperation
17+
import com.onesignal.user.internal.operations.impl.states.NewRecordsState
1718

1819
internal class IdentityOperationExecutor(
1920
private val _identityBackend: IIdentityBackendService,
2021
private val _identityModelStore: IdentityModelStore,
2122
private val _buildUserService: IRebuildUserService,
23+
private val _newRecordState: NewRecordsState,
2224
) : IOperationExecutor {
2325
override val operations: List<String>
2426
get() = listOf(SET_ALIAS, DELETE_ALIAS)
@@ -67,11 +69,15 @@ internal class IdentityOperationExecutor(
6769
NetworkUtils.ResponseStatusType.UNAUTHORIZED ->
6870
ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED)
6971
NetworkUtils.ResponseStatusType.MISSING -> {
70-
val operations = _buildUserService.getRebuildOperationsIfCurrentUser(lastOperation.appId, lastOperation.onesignalId)
71-
if (operations == null) {
72+
if (ex.statusCode == 404 && _newRecordState.isInMissingRetryWindow(lastOperation.onesignalId)) {
73+
return ExecutionResponse(ExecutionResult.FAIL_RETRY)
74+
}
75+
76+
val rebuildOps = _buildUserService.getRebuildOperationsIfCurrentUser(lastOperation.appId, lastOperation.onesignalId)
77+
if (rebuildOps == null) {
7278
return ExecutionResponse(ExecutionResult.FAIL_NORETRY)
7379
} else {
74-
return ExecutionResponse(ExecutionResult.FAIL_RETRY, operations = operations)
80+
return ExecutionResponse(ExecutionResult.FAIL_RETRY, operations = rebuildOps)
7581
}
7682
}
7783
}
@@ -103,10 +109,14 @@ internal class IdentityOperationExecutor(
103109
NetworkUtils.ResponseStatusType.UNAUTHORIZED ->
104110
ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED)
105111
NetworkUtils.ResponseStatusType.MISSING -> {
106-
// This means either the User or the Alias was already
107-
// deleted, either way the end state is the same, the
108-
// alias no longer exists on that User.
109-
ExecutionResponse(ExecutionResult.SUCCESS)
112+
return if (ex.statusCode == 404 && _newRecordState.isInMissingRetryWindow(lastOperation.onesignalId)) {
113+
ExecutionResponse(ExecutionResult.FAIL_RETRY)
114+
} else {
115+
// This means either the User or the Alias was already
116+
// deleted, either way the end state is the same, the
117+
// alias no longer exists on that User.
118+
ExecutionResponse(ExecutionResult.SUCCESS)
119+
}
110120
}
111121
}
112122
}

0 commit comments

Comments
 (0)