diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/helpers/ContinuationResumeGuard.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/helpers/ContinuationResumeGuard.kt index 498ddac97..3158a5caf 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/helpers/ContinuationResumeGuard.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/helpers/ContinuationResumeGuard.kt @@ -1,9 +1,8 @@ package dev.hyo.openiap.helpers import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.InternalCoroutinesApi import java.util.concurrent.atomic.AtomicBoolean -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /** * Atomically completes a cancellable continuation at most once. @@ -26,16 +25,18 @@ internal class ContinuationResumeGuard( } } + @OptIn(InternalCoroutinesApi::class) fun resume(value: T) { - if (didComplete.compareAndSet(false, true)) { - continuation.resume(value) - } + val token = continuation.tryResume(value, null) ?: return + didComplete.set(true) + continuation.completeResume(token) } + @OptIn(InternalCoroutinesApi::class) fun resumeWithException(error: Throwable) { - if (didComplete.compareAndSet(false, true)) { - continuation.resumeWithException(error) - } + val token = continuation.tryResumeWithException(error) ?: return + didComplete.set(true) + continuation.completeResume(token) } } diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/helpers/ContinuationResumeGuardTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/helpers/ContinuationResumeGuardTest.kt new file mode 100644 index 000000000..9088b9fbe --- /dev/null +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/helpers/ContinuationResumeGuardTest.kt @@ -0,0 +1,29 @@ +package dev.hyo.openiap.helpers + +import kotlin.coroutines.resume +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ContinuationResumeGuardTest { + @Test + fun `resumeGuard ignores late completion after continuation already resumed`() = runTest { + var captured: CancellableContinuation? = null + val deferred = async(start = CoroutineStart.UNDISPATCHED) { + suspendCancellableCoroutine { continuation -> + captured = continuation + } + } + val continuation = checkNotNull(captured) + val guard = continuation.resumeGuard() + + continuation.resume(Unit) + deferred.await() + + guard.resumeWithException(IllegalStateException("late product query failure")) + guard.resume(Unit) + } +}