Skip to content

Commit f106ff3

Browse files
committed
Guava ListenableFuture support
1 parent 489cac2 commit f106ff3

File tree

6 files changed

+397
-2
lines changed

6 files changed

+397
-2
lines changed

integration/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ This directory contains modules that provide integration with various asynchrono
66

77
* [kotlinx-coroutines-jdk8](kotlinx-coroutines-jdk8/README.md) -- extensions for JDK8 `CompletableFuture` (Android API level 24).
88
* [kotlinx-coroutines-nio](kotlinx-coroutines-nio/README.md) -- extensions for asynchronous IO on JDK7+ (Android O Preview).
9+
* [kotlinx-coroutines-guava](kotlinx-coroutines-guava/README.md) -- integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained).
910

1011
## Contributing
1112

1213
Follow the following simple guidelines when contributing integration with your favorite library:
1314

1415
* Keep it simple and general. Ideally it should fit into a single file. If it does not fit, then consider
1516
a separate GitHub project to host this integration.
16-
* Follow the example of other modules. Don't fear to cut-and-paste [kotlinx-coroutines-jdk8](kotlinx-coroutines-jdk8)
17-
module for a start.
17+
* Follow the example of other modules.
18+
Cut-and-paste [kotlinx-coroutines-guava](kotlinx-coroutines-guava) module as a template.
1819
* Write tests and documentation, include top-level README.md with short overview and example.
1920
* Include it into the list of modules in this file and to the top-level [pom.xml](../pom.xml).
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Module kotlinx-coroutines-guava
2+
3+
Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained).
4+
5+
Coroutine builders:
6+
7+
| **Name** | **Result** | **Scope** | **Description**
8+
| -------- | ---------- | ---------- | ---------------
9+
| [future] | [ListenableFuture][com.google.common.util.concurrent.ListenableFuture] | [CoroutineScope] | Returns a single value with the future result
10+
11+
Extension functions:
12+
13+
| **Name** | **Description**
14+
| -------- | ---------------
15+
| [ListenableFuture.await][com.google.common.util.concurrent.ListenableFuture.await] | Awaits for completion of the future (cancellable)
16+
| [Deferred.asListenableFuture][kotlinx.coroutines.experimental.Deferred.asListenableFuture] | Converts a deferred value to the future
17+
18+
## Example
19+
20+
Given the following functions defined in some Java API based on Guava:
21+
22+
```java
23+
public ListenableFuture<Image> loadImageAsync(String name); // starts async image loading
24+
public Image combineImages(Image image1, Image image2); // synchronously combines two images using some algorithm
25+
```
26+
27+
We can consume this API from Kotlin coroutine to load two images and combine then asynchronously.
28+
The resulting function returns `ListenableFuture<Image>` for ease of use back from Guava-based Java code.
29+
30+
```kotlin
31+
fun combineImagesAsync(name1: String, name2: String): ListenableFuture<Image> = future {
32+
val future1 = loadImageAsync(name1) // start loading first image
33+
val future2 = loadImageAsync(name2) // start loading second image
34+
combineImages(future1.await(), future2.await()) // wait for both, combine, and return result
35+
}
36+
```
37+
38+
Note, that this module should be used only for integration with existing Java APIs based on `ListenableFuture`.
39+
Writing pure-Kotlin code that uses `ListenableFuture` is highly not recommended, since the resulting APIs based
40+
on the futures are quite error-prone. See the discussion on
41+
[Asynchronous Programming Styles](https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md#asynchronous-programming-styles)
42+
for details on general problems pertaining to any future-based API and keep in mind that `ListenableFuture` exposes
43+
a _blocking_ method
44+
[get](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html#get--)
45+
that makes it especially bad choice for coroutine-based Kotlin code.
46+
47+
# Package kotlinx.coroutines.experimental.future
48+
49+
Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained).
50+
51+
<!--- SITE_ROOT https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core -->
52+
<!--- DOCS_ROOT kotlinx-coroutines-core/target/dokka/kotlinx-coroutines-core -->
53+
<!--- INDEX kotlinx.coroutines.experimental -->
54+
[CoroutineScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-coroutine-scope/index.html
55+
<!--- SITE_ROOT https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava -->
56+
<!--- DOCS_ROOT integration/kotlinx-coroutines-guava/target/dokka/kotlinx-coroutines-guava -->
57+
<!--- INDEX kotlinx.coroutines.experimental.guava -->
58+
<!--- END -->
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2016-2017 JetBrains s.r.o.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
19+
<modelVersion>4.0.0</modelVersion>
20+
21+
<parent>
22+
<groupId>org.jetbrains.kotlinx</groupId>
23+
<artifactId>kotlinx-coroutines</artifactId>
24+
<version>0.16-SNAPSHOT</version>
25+
<relativePath>../../pom.xml</relativePath>
26+
</parent>
27+
28+
<artifactId>kotlinx-coroutines-guava</artifactId>
29+
<packaging>jar</packaging>
30+
31+
<build>
32+
<sourceDirectory>src/main/kotlin</sourceDirectory>
33+
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
34+
</build>
35+
36+
<dependencies>
37+
<!-- dependency on coroutines core -->
38+
<dependency>
39+
<groupId>org.jetbrains.kotlinx</groupId>
40+
<artifactId>kotlinx-coroutines-core</artifactId>
41+
<version>${project.version}</version>
42+
</dependency>
43+
<!-- coroutines test framework dependency -->
44+
<dependency>
45+
<groupId>org.jetbrains.kotlinx</groupId>
46+
<artifactId>kotlinx-coroutines-core</artifactId>
47+
<version>${project.version}</version>
48+
<classifier>tests</classifier>
49+
<scope>test</scope>
50+
</dependency>
51+
<!-- 3rd party dependencies -->
52+
<dependency>
53+
<groupId>com.google.guava</groupId>
54+
<artifactId>guava</artifactId>
55+
<version>21.0</version>
56+
</dependency>
57+
</dependencies>
58+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2016-2017 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package kotlinx.coroutines.experimental.guava
18+
19+
import com.google.common.util.concurrent.AbstractFuture
20+
import com.google.common.util.concurrent.FutureCallback
21+
import com.google.common.util.concurrent.Futures
22+
import com.google.common.util.concurrent.ListenableFuture
23+
import kotlinx.coroutines.experimental.*
24+
import kotlin.coroutines.experimental.Continuation
25+
import kotlin.coroutines.experimental.CoroutineContext
26+
27+
/**
28+
* Starts new coroutine and returns its results an an implementation of [ListenableFuture].
29+
* This coroutine builder uses [CommonPool] context by default.
30+
*
31+
* The running coroutine is cancelled when the resulting future is cancelled or otherwise completed.
32+
* If the [context] for the new coroutine is omitted or is explicitly specified but does not include a
33+
* coroutine interceptor, then [CommonPool] is used.
34+
* See [CoroutineDispatcher] for other standard [context] implementations that are provided by `kotlinx.coroutines`.
35+
* The [context][CoroutineScope.context] of the parent coroutine from its [scope][CoroutineScope] may be used,
36+
* in which case the [Job] of the resulting coroutine is a child of the job of the parent coroutine.
37+
*
38+
* By default, the coroutine is immediately scheduled for execution.
39+
* Other options can be specified via `start` parameter. See [CoroutineStart] for details.
40+
* A value of [CoroutineStart.LAZY] is not supported
41+
* (since `ListenableFuture` framework does not provide the corresponding capability) and
42+
* produces [IllegalArgumentException].
43+
*
44+
* See [newCoroutineContext] for a description of debugging facilities that are available for newly created coroutine.
45+
*
46+
* @param context context of the coroutine
47+
* @param start coroutine start option
48+
* @param block the coroutine code
49+
*/
50+
public fun <T> future(
51+
context: CoroutineContext = CommonPool,
52+
start: CoroutineStart = CoroutineStart.DEFAULT,
53+
block: suspend CoroutineScope.() -> T
54+
): ListenableFuture<T> {
55+
require(!start.isLazy) { "$start start is not supported" }
56+
val newContext = newCoroutineContext(CommonPool + context)
57+
val job = Job(newContext[Job])
58+
val future = ListenableFutureCoroutine<T>(newContext + job)
59+
job.cancelFutureOnCompletion(future)
60+
start(block, receiver=future, completion=future) // use the specified start strategy
61+
return future
62+
}
63+
64+
private class ListenableFutureCoroutine<T>(
65+
override val context: CoroutineContext
66+
) : AbstractFuture<T>(), Continuation<T>, CoroutineScope {
67+
override val isActive: Boolean get() = context[Job]!!.isActive
68+
override fun resume(value: T) { set(value) }
69+
override fun resumeWithException(exception: Throwable) { setException(exception) }
70+
override fun interruptTask() { context[Job]!!.cancel() }
71+
}
72+
73+
/**
74+
* Converts this deferred value to the instance of [ListenableFuture].
75+
* The deferred value is cancelled when the resulting future is cancelled or otherwise completed.
76+
*/
77+
public fun <T> Deferred<T>.asListenableFuture(): ListenableFuture<T> = DeferredListenableFuture<T>(this)
78+
79+
private class DeferredListenableFuture<T>(
80+
private val deferred: Deferred<T>
81+
) : AbstractFuture<T>() {
82+
init {
83+
deferred.invokeOnCompletion {
84+
try {
85+
set(deferred.getCompleted())
86+
} catch (exception: Exception) {
87+
setException(exception)
88+
}
89+
}
90+
}
91+
override fun interruptTask() { deferred.cancel() }
92+
}
93+
94+
/**
95+
* Awaits for completion of the future without blocking a thread.
96+
*
97+
* This suspending function is cancellable.
98+
* If the [Job] of the current coroutine is completed while this suspending function is waiting, this function
99+
* stops waiting for the future and immediately resumes with [CancellationException].
100+
*
101+
* Note, that `ListenableFuture` does not support removal of installed listeners, so on cancellation of this wait
102+
* a few small objects will remain in the `ListenableFuture` list of listeners until the future completes. However, the
103+
* care is taken to clear the reference to the waiting coroutine itself, so that its memory can be released even if
104+
* the future never completes.
105+
*/
106+
public suspend fun <T> ListenableFuture<T>.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation<T> ->
107+
val callback = ContinuationCallback(cont)
108+
Futures.addCallback(this, callback)
109+
cont.invokeOnCompletion {
110+
callback.cont = null // clear the reference to continuation from the future's callback
111+
}
112+
}
113+
114+
private class ContinuationCallback<T>(
115+
@Volatile @JvmField var cont: Continuation<T>?
116+
) : FutureCallback<T> {
117+
override fun onSuccess(result: T?) { cont?.resume(result as T) }
118+
override fun onFailure(t: Throwable) { cont?.resumeWithException(t) }
119+
}

0 commit comments

Comments
 (0)