Repository navigation
test: [SDK-5538] guard stand-ins and the public API against drift #2828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bc5c01b
135f3a2
58221a4
9ef18e1
1624e1b
a05ffa9
b7f9878
5b2ad36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,9 +78,9 @@ interface IOneSignal { | |
| /** | ||
| * Initialize the OneSignal SDK. This should be called during startup of the application. | ||
| * | ||
| * @param context The Android context the SDK should use. Required: a null context is logged and init does not start. | ||
| * @param context The Android context the SDK should use. Required: a null context is logged and init does not start. The log says the SDK is not initialized. | ||
| * @param appId Your OneSignal app ID. Always pass it: a null or invalid value falls back to the app ID | ||
| * cached by an earlier launch, and with none cached init fails and logs. | ||
| * cached by an earlier launch, and with none cached init fails and logs that the SDK is not initialized. | ||
| * | ||
| * @return true once initialization has started (or is already running or done), false for a | ||
| * null [context] or an immediate startup failure. Check [isInitialized] for the outcome. | ||
|
|
@@ -93,9 +93,9 @@ interface IOneSignal { | |
| /** | ||
| * Initialize the OneSignal SDK, suspend until initialization is completed | ||
| * | ||
| * @param context The Android context the SDK should use. | ||
| * @param context The Android context the SDK should use. Required: a null context is logged and init does not start. The log says the SDK is not initialized. | ||
| */ | ||
| suspend fun initWithContext(context: Context): Boolean | ||
| suspend fun initWithContext(context: Context?): Boolean | ||
|
|
||
| /** | ||
| * Login to OneSignal under the user identified by the [externalId] provided. The act of | ||
|
|
@@ -113,17 +113,17 @@ interface IOneSignal { | |
| * subscriptions from the current user (if there is one) to the newly logged in user. This is | ||
| * because both Push and IAM are owned by the device. | ||
| * | ||
| * @param externalId The external ID of the user to log in. Must be non-empty: an empty value is ignored and logged. | ||
| * @param externalId The external ID of the user to log in. Required: null or empty is ignored and logged. A null value's log says the SDK is not initialized when it is not. | ||
| * @param jwtBearerToken The optional JWT bearer token generated by your backend to establish | ||
| * trust for the login operation. Required when identity verification has been enabled. See | ||
| * [Identity Verification | OneSignal](https://documentation.onesignal.com/docs/identity-verification) | ||
| */ | ||
| fun login( | ||
| externalId: String, | ||
| externalId: String?, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly blocking: externalId is now |
||
| jwtBearerToken: String? = null, | ||
| ) | ||
|
|
||
| fun login(externalId: String) = login(externalId, null) | ||
| fun login(externalId: String?) = login(externalId, null) | ||
|
|
||
| /** | ||
| * Logout the user previously logged in via [login]. The [user] property now references | ||
|
|
@@ -139,11 +139,11 @@ interface IOneSignal { | |
| * [IUserJwtInvalidatedListener.onUserJwtInvalidated] callback). Stores the JWT and | ||
| * wakes the operation queue so any deferred ops can dispatch with the fresh token. | ||
| * | ||
| * @param externalId The external ID the JWT belongs to. Must be non-empty. | ||
| * @param externalId The external ID the JWT belongs to. Required: null or empty is ignored and logged. A null value's log says the SDK is not initialized when it is not. | ||
| * @param token The new JWT bearer token issued by your backend. Must be non-empty; empty values are ignored and logged. | ||
| */ | ||
| fun updateUserJwt( | ||
| externalId: String, | ||
| externalId: String?, | ||
| token: String, | ||
| ) | ||
|
|
||
|
|
@@ -166,9 +166,9 @@ interface IOneSignal { | |
| /** | ||
| * Initialize the OneSignal SDK, suspend until initialization is completed | ||
| * | ||
| * @param context The Android context the SDK should use. Required: a null context is logged and init does not start. | ||
| * @param context The Android context the SDK should use. Required: a null context is logged and init does not start. The log says the SDK is not initialized. | ||
| * @param appId Your OneSignal app ID. Always pass it: a null or invalid value falls back to the app ID | ||
| * cached by an earlier launch, and with none cached init fails and logs. | ||
| * cached by an earlier launch, and with none cached init fails and logs that the SDK is not initialized. | ||
| * | ||
| * @return true if the SDK could be successfully initialized, false otherwise. | ||
| */ | ||
|
|
@@ -243,13 +243,13 @@ interface IOneSignal { | |
| * Login a user with external ID and optional JWT token (suspend version). | ||
| * Handles initialization automatically. | ||
| * | ||
| * @param externalId The external ID of the user to log in. Must be non-empty: an empty value is ignored and logged. | ||
| * @param externalId The external ID of the user to log in. Required: null or empty is ignored and logged. A null value's log says the SDK is not initialized when it is not. | ||
| * @param jwtBearerToken The optional JWT bearer token generated by your backend to establish | ||
| * trust for the login operation. Required when identity verification has been enabled. | ||
| * See [Identity Verification | OneSignal](https://documentation.onesignal.com/docs/identity-verification) | ||
| */ | ||
| suspend fun loginSuspend( | ||
| externalId: String, | ||
| externalId: String?, | ||
| jwtBearerToken: String? = null, | ||
| ) | ||
|
|
||
|
|
@@ -273,11 +273,11 @@ interface IOneSignal { | |
| * Update the JWT bearer token associated with [externalId] (suspend version). Suspends while | ||
| * initialization is in progress, then stores the JWT; if the SDK is not initialized it logs instead. | ||
| * | ||
| * @param externalId The external ID the JWT belongs to. Must be non-empty. | ||
| * @param externalId The external ID the JWT belongs to. Required: null or empty is ignored and logged. A null value's log says the SDK is not initialized when it is not. | ||
| * @param token The new JWT bearer token issued by your backend. Must be non-empty; empty values are ignored and logged. | ||
| */ | ||
| suspend fun updateUserJwtSuspend( | ||
| externalId: String, | ||
| externalId: String?, | ||
| token: String, | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,11 @@ internal open class DeferredManager<T : Any>( | |
| private val pending = mutableListOf<Pending<T>>() | ||
| private var replayed = false | ||
|
|
||
| // Set for the whole drain. Other threads enqueue instead of touching the real manager, so a remove | ||
| // cannot land before the add that was already copied into the batch being applied. | ||
| private var replaying = false | ||
| private var replayThread: Thread? = null | ||
|
|
||
| // Kept after replay: if init then fails, or a replayed listener calls back in, the gate is not ready | ||
| // but this manager already holds the replayed registrations, so later ones must reach it too. | ||
| private var replayedTo: T? = null | ||
|
|
@@ -55,15 +60,22 @@ internal open class DeferredManager<T : Any>( | |
| ) = forwardOrQueue(member, call) { pending.size < MAX_PENDING && pending.add(Pending(member, target, call)) } | ||
|
|
||
| // Before replay the real manager holds nothing, so a removal only has to cancel the queued add. | ||
| // Once that add has been copied into the batch in flight, the removal itself is queued behind it. | ||
| protected fun unregisterOrCancel( | ||
| member: String, | ||
| addMember: String, | ||
| target: Any, | ||
| call: (T) -> Unit, | ||
| ) = forwardOrQueue(member, call) { | ||
| val index = pending.indexOfLast { it.member == addMember && it.target === target } | ||
| if (index >= 0) pending.removeAt(index) | ||
| true | ||
| if (index >= 0) { | ||
| pending.removeAt(index) | ||
| true | ||
| } else if (replaying) { | ||
| pending.size < MAX_PENDING && pending.add(Pending(member, target, call)) | ||
| } else { | ||
| false | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking: removing a listener that was never added now falls through to |
||
| } | ||
| } | ||
|
|
||
| // [onQueued] runs under the queue lock, so a getter cache it sets always matches what replays. | ||
|
|
@@ -85,13 +97,11 @@ internal open class DeferredManager<T : Any>( | |
| call: (T) -> Unit, | ||
| enqueue: () -> Boolean, | ||
| ) { | ||
| source.peek()?.let { | ||
| call(it) | ||
| return | ||
| } | ||
| val (queued, target) = | ||
| synchronized(pending) { | ||
| if (replayed) false to replayedTo else enqueue() to null | ||
| val applying = replaying && Thread.currentThread() === replayThread | ||
| val finished = replayed && !replaying | ||
| if (applying || finished) false to (replayedTo ?: source.peek()) else enqueue() to null | ||
| } | ||
| when { | ||
| queued -> source.logQueued(member) | ||
|
|
@@ -100,20 +110,52 @@ internal open class DeferredManager<T : Any>( | |
| } | ||
| } | ||
|
|
||
| @Suppress("TooGenericExceptionCaught") | ||
| open fun replay(real: T) { | ||
| val calls = | ||
| synchronized(pending) { | ||
| replayed = true | ||
| replayedTo = real | ||
| pending.toList().also { pending.clear() } | ||
| } | ||
| for (entry in calls) { | ||
| try { | ||
| entry.call(real) | ||
| } catch (e: Exception) { | ||
| Logging.error("Replaying '${entry.member}' threw; that queued call is lost", e) | ||
| if (!beginReplay(real)) return | ||
| try { | ||
| while (true) { | ||
| val batch = takeBatch() | ||
| if (batch.isEmpty()) return | ||
| batch.forEach { deliver(it, real) } | ||
| } | ||
| } finally { | ||
| abandonReplay() | ||
| } | ||
| } | ||
|
|
||
| private fun beginReplay(real: T): Boolean = | ||
| synchronized(pending) { | ||
| if (replayed || replaying) return@synchronized false | ||
| replaying = true | ||
| replayThread = Thread.currentThread() | ||
| replayedTo = real | ||
| true | ||
| } | ||
|
|
||
| private fun takeBatch(): List<Pending<T>> = | ||
| synchronized(pending) { | ||
| if (pending.isNotEmpty()) return@synchronized pending.toList().also { pending.clear() } | ||
| replaying = false | ||
| replayThread = null | ||
| replayed = true | ||
| emptyList() | ||
| } | ||
|
|
||
| @Suppress("TooGenericExceptionCaught") | ||
| private fun deliver(entry: Pending<T>, real: T) { | ||
| try { | ||
| entry.call(real) | ||
| } catch (e: Exception) { | ||
| Logging.error("Replaying '${entry.member}' threw; that queued call is lost", e) | ||
| } | ||
| } | ||
|
|
||
| private fun abandonReplay() { | ||
| synchronized(pending) { | ||
| if (!replaying) return@synchronized | ||
| replaying = false | ||
| replayThread = null | ||
| replayed = true | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "says the SDK is not initialized when it is not" reads like the log is wrong. Maybe "while the SDK is not initialized, the log says so"?