Skip to content

Commit f62d35b

Browse files
committed
Properly handle null values in ListenableFuture
Fixes #1510
1 parent 385d68c commit f62d35b

File tree

2 files changed

+26
-27
lines changed

2 files changed

+26
-27
lines changed

integration/kotlinx-coroutines-guava/src/ListenableFuture.kt

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
package kotlinx.coroutines.guava
66

77
import com.google.common.util.concurrent.*
8-
import com.google.common.util.concurrent.internal.InternalFutureFailureAccess
9-
import com.google.common.util.concurrent.internal.InternalFutures
10-
import java.util.concurrent.*
11-
import kotlin.coroutines.*
8+
import com.google.common.util.concurrent.internal.*
129
import kotlinx.coroutines.*
10+
import java.util.concurrent.*
1311
import java.util.concurrent.CancellationException
12+
import kotlin.coroutines.*
1413

1514
/**
1615
* Starts [block] in a new coroutine and returns a [ListenableFuture] pointing to its result.
@@ -120,14 +119,7 @@ public fun <T> ListenableFuture<T>.asDeferred(): Deferred<T> {
120119
// handle interruption.
121120
if (isDone) {
122121
return try {
123-
val value = Uninterruptibles.getUninterruptibly(this)
124-
if (value == null) {
125-
CompletableDeferred<T>().also {
126-
it.completeExceptionally(KotlinNullPointerException())
127-
}
128-
} else {
129-
CompletableDeferred(value)
130-
}
122+
CompletableDeferred(Uninterruptibles.getUninterruptibly(this))
131123
} catch (e: CancellationException) {
132124
CompletableDeferred<T>().also { it.cancel(e) }
133125
} catch (e: ExecutionException) {
@@ -142,7 +134,9 @@ public fun <T> ListenableFuture<T>.asDeferred(): Deferred<T> {
142134
val deferred = CompletableDeferred<T>()
143135
Futures.addCallback(this, object : FutureCallback<T> {
144136
override fun onSuccess(result: T?) {
145-
deferred.complete(result!!)
137+
// Here we work with flexible types, so we unchecked cast to trick the type system
138+
@Suppress("UNCHECKED_CAST")
139+
deferred.complete(result as T)
146140
}
147141

148142
override fun onFailure(t: Throwable) {

integration/kotlinx-coroutines-guava/test/ListenableFutureTest.kt

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -436,31 +436,36 @@ class ListenableFutureTest : TestBase() {
436436
}
437437

438438
@Test
439-
fun testFutureCompletedWithNullAsDeferred() = runTest {
439+
fun testFutureCompletedWithNullFastPathAsDeferred() = runTest {
440440
val executor = MoreExecutors.listeningDecorator(ForkJoinPool.commonPool())
441-
val future = executor.submit(Callable { null })
442-
val deferred = GlobalScope.async {
441+
val future = executor.submit(Callable<Int> { null }).also { it.get() }
442+
assertNull(future.asDeferred().await())
443+
}
444+
445+
@Test
446+
fun testFutureCompletedWithNullSlowPathAsDeferred() = runTest {
447+
val latch = CountDownLatch(1)
448+
val executor = MoreExecutors.listeningDecorator(ForkJoinPool.commonPool())
449+
450+
val future = executor.submit(Callable<Int> {
451+
latch.await()
452+
null
453+
})
454+
455+
val awaiter = async(start = CoroutineStart.UNDISPATCHED) {
443456
future.asDeferred().await()
444457
}
445458

446-
try {
447-
deferred.await()
448-
expectUnreached()
449-
} catch (e: Throwable) {
450-
assertTrue(e is KotlinNullPointerException)
451-
}
459+
latch.countDown()
460+
assertNull(awaiter.await())
452461
}
453462

454463
@Test
455464
fun testThrowingFutureAsDeferred() = runTest {
456465
val executor = MoreExecutors.listeningDecorator(ForkJoinPool.commonPool())
457466
val future = executor.submit(Callable { throw TestException() })
458-
val deferred = GlobalScope.async {
459-
future.asDeferred().await()
460-
}
461-
462467
try {
463-
deferred.await()
468+
future.asDeferred().await()
464469
expectUnreached()
465470
} catch (e: Throwable) {
466471
assertTrue(e is TestException)

0 commit comments

Comments
 (0)