From 7ab57ca7cd0d9be6ff6e5874208bf80564b952be Mon Sep 17 00:00:00 2001 From: Marian Date: Thu, 9 Jul 2026 11:32:42 +0300 Subject: [PATCH] fix: harden continuation resume guard --- .../helpers/ContinuationResumeGuard.kt | 17 ++++++----- .../helpers/ContinuationResumeGuardTest.kt | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 packages/google/openiap/src/test/java/dev/hyo/openiap/helpers/ContinuationResumeGuardTest.kt 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) + } +}