how to use suspendCoroutine to turn java 7 future into kotlin suspending function
Asked Answered
L

3

15

What is the best approach to wrap java 7 futures inside a kotlin suspend function? Is there a way to convert a method returning Java 7 futures into a suspending function?

The process is pretty straightforward for arbitrary callbacks or java 8 completablefutures, as illustrated for example here: * https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md#suspending-functions

In these cases, there is a hook that is triggered when the future is done, so it can be used to resume the continuation as soon as the value of the future is ready (or an exception is triggered).

Java 7 futures however don't expose a method that is invoked when the computation is over.

Converting a Java 7 future to a Java 8 completable future is not an option in my codebase.

Of course, i can create a suspend function that calls future.get() but this would be blocking, which breaks the overall purpose of using coroutine suspension.

Another option would be to submit a runnable to a new thread executor, and inside the runnable call future.get() and invoke a callback. This wrapper will make the code looks like "non-blocking" from the consumer point of view, the coroutine can suspend, but under the hood we are still writing blocking code and we are creating a new thread just for the sake of blocking it

Lundin answered 11/6, 2018 at 8:27 Comment(0)
L
17

Java 7 future is blocking. It is not designed for asynchronous APIs and does not provide any way to install a callback that is invoked when the future is complete. It means that there is no direct way to use suspendCoroutine with it, because suspendCoroutine is designed for use with asynchronous callback-using APIs.

However, if your code is, in fact, running under JDK 8 or a newer version, there are high chances that the actual Future instance that you have in your code happens to implement CompletionStage interface at run-time. You can try to cast it to CompletionStage and use ready-to-use CompletionStage.await extension from kotlinx-coroutines-jdk8 module of kotlinx.coroutines library.

Limpid answered 15/6, 2018 at 12:37 Comment(1)
However, Java 7 future does provide a way to check if its complete. This, combined with a coroutine who's job is to do this check on some interval (code in other answer) can effectively turn a Java 7 Future into something that behaves like a proper Future monad (i.e. CompletableFuture) which has been wrapped in a suspending function.Duce
D
1

Of course Roman is right that a Java Future does not let you provide a callback for when the work is done.

However, it does give you a way to check if the work is done, and if it is, then calling .get() won't block.

Luckily for us, we also have a cheap way to divert a thread to quickly do a poll check via coroutines.

Let's write that polling logic and also vend it as an extension method:

suspend fun <T> Future<T>.wait(): T {
    while(!isDone)
        delay(1) // or whatever you want your polling frequency to be
    return get()
}

Then to use:

fun someBlockingWork(): Future<String> { ... }

suspend fun useWork() {
    val result = someBlockingWork().wait()
    println("Result: $result")
}

So we have millisecond-response time to our Futures completing without using any extra threads.


And of course you'll want to add some upper bound to use as a timeout so you don't end up waiting forever. In that case, we can update the code just a little:

suspend fun <T> Future<T>.wait(timeoutMs: Int = 60000): T? {
    val start = System.currentTimeMillis()
    while (!isDone) {
        if (System.currentTimeMillis() - start > timeoutMs)
            return null
        delay(1)
    }
    return get()
}
Duce answered 20/10, 2018 at 1:38 Comment(0)
M
1

You should be now be able to do this by creating another coroutine in the same scope that cancels the Future when the coroutine is cancelled.

withContext(Dispatchers.IO) {
  val future = getSomeFuture()

  coroutineScope {
    val cancelJob = launch {
      suspendCancellableCoroutine<Unit> { cont ->
        cont.invokeOnCancellation {
          future.cancel(true)
        }
      }
    }  

    future.get().also {
      cancelJob.cancel()
    }
  }
}  
Mavis answered 14/8, 2020 at 7:16 Comment(2)
I have run into a problem with this solution: future.cancel() is always called, event if the future has completed normally. I have no control of the implementation of the Future and this cancellation caused some weird behavior. I changed the code a bit by adding a completed flag, which is set to true after future is completed. I check this flag inside invokeOnCancellation and cancel the future only if it is false.Haig
Also I think we can wrap future.get() into runInterruptible { } block.Haig

© 2022 - 2024 — McMap. All rights reserved.