Replacing GlobalScope.launch with something better - kotlin

I'm refactoring the following bit of code that's wrapping a CompletableFuture API into something that can be used with Coroutines, but it's using GlobalScope.launch { ... } which is discouraged:
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
val cf = CompletableFuture<T>()
try {
this.connectionPool.inTransaction { connection ->
GlobalScope.launch {
try {
cf.complete(f(connection))
} catch (e: Throwable) {
cf.completeExceptionally(e)
}
}
cf
}
} catch (e: Throwable) {
log.error(e.message ?: "", e)
cf.completeExceptionally(e)
}
return cf.await()
}
Getting rid of the CompletableFuture and replacing it with CompletableDeferred is the easy part:
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
val cdf = CompletableDeferred<T>()
try {
connectionPool.inTransaction { connection ->
GlobalScope.launch {
try {
cdf.complete(f(connection))
} catch (e: Throwable) {
cdf.completeExceptionally(e)
}
}
cdf.asCompletableFuture()
}
} catch (e: Throwable) {
log.error(e.message ?: "", e)
cdf.completeExceptionally(e)
}
return cdf.await()
}
The inTransaction API expects a CompletableFuture, I'm assuming this is for backwards compatibility with Java
override fun <A> inTransaction(f: (Connection) -> CompletableFuture<A>):
CompletableFuture<A> =
objectPool.use(configuration.executionContext) { it.inTransaction(f) }
Since I'm outside a CoroutineScope, I can't just call launch { ... } and since f is a suspend function, that section needs to be inside a CoroutineScope
Wrapping the connectionPool.inTransaction inside a coroutineScope in order to replace the GlobalScope locks up when it runs ...
try {
coroutineScope {
connectionPool.inTransaction { connection ->
launch {
try {
cdf.complete(f(connection))
} catch (e: Throwable) {
cdf.completeExceptionally(e)
}
}
cdf.asCompletableFuture()
}
}
} catch (e: Throwable) {
Similarly with
try {
coroutineScope {
connectionPool.inTransaction { connection ->
async {
try {
cdf.complete(f(connection))
} catch (e: Throwable) {
cdf.completeExceptionally(e)
}
}
cdf.asCompletableFuture()
}
}
} catch (e: Throwable) {
Adding some good ol' println debugging:
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
val cdf = CompletableDeferred<T>()
println("1")
try {
println("2")
coroutineScope {
println("3")
connectionPool.inTransaction { connection ->
println("4")
launch {
println("5")
try {
println("6")
cdf.complete(f(connection))
println("7")
} catch (e: Throwable) {
println("8")
cdf.completeExceptionally(e)
println("9")
}
}
println("10")
cdf.asCompletableFuture()
}
}
} catch (e: Throwable) {
log.error(e.message ?: "", e)
cdf.completeExceptionally(e)
}
println("11")
return cdf.await()
}
Outputs:
1
2
3
11
4
10
followed by a stacktrace that the query timed out
timeout query item <postgres-connection-2> after 73751 ms and was not cleaned by connection as it should, will destroy it - timeout is 30000
This means the code inside the launch is never executing, similarly with async, I'm assuming some thread is getting blocked somewhere.
Replacing the GlobalScope.launch with CoroutineScope(Dispatchers.IO).launch { ... } works (similarly with Dispatchers.Unconfined)), but is this the correct solution here?
If CompletableFuture is thread-blocking, then this is probably better than the GlobalScope.launch solution ...
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
val cdf = CompletableDeferred<T>()
try {
connectionPool.inTransaction { connection ->
CoroutineScope(Dispatchers.IO).launch {
try {
cdf.complete(f(connection))
} catch (e: Throwable) {
cdf.completeExceptionally(e)
}
}
cdf.asCompletableFuture()
}
} catch (e: Throwable) {
log.error(e.message ?: "", e)
cdf.completeExceptionally(e)
}
return cdf.await()
}
Any suggestions on what the correct way is to get rid of that GlobalScope.launch?

It's pretty complicated, but I believe the cause of launch()/async() not executing is that their parent coroutineScope() already finished at this point in time. Notice "11" happens before "4", meaning that you invoked launch() after going out of coroutineScope(). Which makes sense, because inTransaction() starts asynchronous operation, so it returns immediately, without waiting for the inner code. To fix this, you just need to move cdf.await() inside coroutineScope().
Another thing that concerns me is that you await on completable that you created by yourself and not the one returned from inTransaction(). Note that it may be a totally different CompletableFuture and in that case you actually return before the operation completes.
Also, I'm not sure if this manual exception handling for completable is really necessary. async() already performs exception handling and wraps the result as CompleteableDeferred, then it is converted to CompletableFuture which also wraps exceptions. The only thing we have to do is to replace coroutineScope() with supervisorScope(). Otherwise, async() would automatically signal coroutineScope() to fail, so the exception handling would totally bypass inTransaction() function.
Try this code:
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
return supervisorScope {
connectionPool.inTransaction { connection ->
async { f(connection) }.asCompletableFuture()
}.await()
}
}

Related

Launch coroutine in try catch block crashes

I could not understand the difference between these two functions. Why the func2 crashes program while func1 can caught the exception ?
fun main() {
runBlocking {
func1() //Prints exception
func2() //Program crashes
}
}
fun CoroutineScope.func1() {
launch {
try {
throw IllegalArgumentException("error")
} catch (t: Throwable) {
println(t)
}
}
}
fun CoroutineScope.func2() {
try {
launch {
throw IllegalArgumentException("error")
}
} catch (t: Throwable) {
println(t)
}
}
The code inside "launch" block runs on a separate Coroutine with a different context. The external try/catch cannot catch the exception happening. You need to have try/catch in one block, like your func1.

After a coroutine scope is canceled, can it still be used again?

When we have a coroutine scope, when it is canceled, can it be used again?
e.g. for the below, when I have scope.cancel, the scope.launch no longer work
#Test
fun testingLaunch() {
val scope = MainScope()
runBlocking {
scope.cancel()
scope.launch {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}.join()
println("Finished")
}
}
Similarly, when we have scope.cancel before await called,
#Test
fun testingAsync() {
val scope = MainScope()
runBlocking {
scope.cancel()
val defer = scope.async {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
defer.await()
println("Finished")
}
}
It will not execute. Instead, it will crash with
kotlinx.coroutines.JobCancellationException: Job was cancelled
; job=SupervisorJobImpl{Cancelled}#39529185
at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:1579)
at kotlinx.coroutines.CoroutineScopeKt.cancel(CoroutineScope.kt:217)
at kotlinx.coroutines.CoroutineScopeKt.cancel$default(CoroutineScope.kt:215)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:241)
at |b|b|b(Coroutine boundary.|b(|b)
at kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:101)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:254)
Caused by: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelled}#39529185
at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:1579)
at kotlinx.coroutines.CoroutineScopeKt.cancel(CoroutineScope.kt:217)
at kotlinx.coroutines.CoroutineScopeKt.cancel$default(CoroutineScope.kt:215)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:241)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
Is it true, a canceled coroutine scope cannot be used for launch or async anymore?
Instead of using CoroutineScope for cancelling all of the launched jobs in it, you may want to use the underlying CoroutineContext with its cancelChildren() method which doesn't affect the Job state (which is not true for plain cancel() method) and allows to continue launching new coroutines after being invoked.
Following up on #Alex Bonel response.
For example you have a method like
fun doApiCall() {
viewModelScope.launch {
// do api call here
}
}
You can call doApiCall() again and again
viewModelScope.coroutineContext.cancelChildren()
doApiCall()
Calling doApiCall() will not have any effect.
viewModelScope.coroutineContext.cancel()
doApiCall() // would not call
I am using that in compose and it is slightly different.
Define coroutine scope in compose
val coroutineScope = rememberCoroutineScope()
Launch coroutine
coroutineScope.launch(Dispatchers.Main) { //Perform your operation }
Cancel all coroutines that belong to coroutineScope
coroutineScope.coroutineContext.cancelChildren()
Related to lifecycleScope comment above, I'm not sure how common cancellation like this would be in practice? fwiw something like following will work:
val scope = MainScope()
runBlocking {
val job1 = scope.launch {
try {
println("Start Launch 1")
delay(200)
println("End Launch 1")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
job1.cancel()
val job2 = scope.launch {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
job2.join()
println("Finished")
}
This particular example will print
Start Launch 1
Cancellation Exception
Start Launch 2
End Launch 2
Finished
I needed to collect the event just once and then stop listening. I came up with this solution:
/**
* Collect the value once and stop listening (one-time events)
*/
suspend fun <T> Flow<T>.collectOnce(action: suspend (value: T) -> Unit) {
try {
coroutineScope {
collectLatest {
action(it)
this#coroutineScope.cancel()
}
}
} catch (e: CancellationException) { }
}
You can use it like this:
viewModelScope.launch {
myStateFlow.collectOnce {
// Code to run
}
}

How use the coroutine within extend function in the latest kotlin-couroutine

In the example: kotlin-examples/coroutines/src/main/kotlin/movierating/App.kt
There is the flowing code:
fun Route.coroutineHandler(fn: suspend (RoutingContext) -> Unit) {
handler { ctx ->
launch(ctx.vertx().dispatcher()) {
try {
fn(ctx)
} catch (e: Exception) {
ctx.fail(e)
}
}
}
}
In the latest kotlin-coroutine,to invoke launch must depend on a CoroutineScope;
So the launch can't be invoked in the extend function Route.coroutineHandler() ;
If always use GlobalScope.launch() to start couroutine,how manage the life-cycle properly?
So I use the flowing method:
interface SuspendHandler<E>: Handler<E>,CoroutineScope {
override fun handle(event: E) {
launch {
suspendHandle(event)
}
}
suspend fun suspendHandle(event: E)
}
fun <E> vertxSuspendHandler(vertx: Vertx = getDefaultVertx(),
block:suspend CoroutineScope.(E)->Unit): SuspendHandler<E>{
return object: SuspendHandler<E> {
override val coroutineContext: CoroutineContext
get() = vertx.dispatcher()
override suspend fun suspendHandle(event: E) {
block(event)
}
}
}
I don't know how use extend function in the latest coroutine api;
You can achieve that by adding the following extension:
fun Route.suspendHandler(requestHandler: suspend (RoutingContext) -> Unit) {
handler { ctx ->
CoroutineScope(ctx.vertx().dispatcher()).launch {
requestHandler(ctx)
}.invokeOnCompletion {
it?.run { ctx.fail(it) }
}
}
}
You can place this extension anywhere in the code.

GlobalScope coroutine not logging error to console

Why is the code below not logging to the console a TimeoutCancellationException?
#Test fun noExceptionLogged(){
GlobalScope.launch{
withTimeout(4000) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
Thread.sleep(20_000)
}
It seems like it works like this due to GlobalScope nature. It cannot be canceled so it swallows CancellationException.
GlobalScope.launch { // won't print anything
throw CancellationException()
}
GlobalScope.launch { // will print stacktrace
throw RuntimeException()
}
runBlocking { // will print stackrace
throw RuntimeException()
}
GlobalScope.launch { // will print "Hello!"
try {
throw CancellationException()
} catch (e: Exception) {
println("Hello!")
}
}

Converting "Callable<T>" Java method to Kotlin

I'm trying to convert a Java method:
private <T> Callable<T> createCallable(final Callable<T> task) {
return () -> {
try {
return task.call();
} catch (Exception e) {
handle(e);
throw e;
}
};
}
from the following Java file ExceptionHandlingAsyncTaskExecutor.java into Kotlin.
The code gets converted automatically using IntelliJ IDEA into:
private fun <T> createCallable(task: Callable<T>): Callable<T> {
return {
try {
return task.call()
} catch (e: Exception) {
handle(e)
throw e
}
}
}
which is not correct. But I have to idea what the correct implementation for this should be. Any ideas?
I think this is an Kotlin converter bug. It converted your code to () -> T instead of Callable<T> (which is basically the same but these are actually different types).
This is the working code
private fun <T> createCallable(task: Callable<T>): Callable<T> {
return Callable {
try {
task.call()
} catch (e: Exception) {
handle(e)
throw e
}
}
}
This is how I did it, might be too verbose, but it works. I also implement a handle function.
import java.util.concurrent.*
private fun <T> createCallable(task: Callable<T>): Callable<T> {
return object : Callable<T> {
override fun call(): T {
try {
return task.call()
} catch (e: Exception) {
handle(e)
throw e
}
}
}
}
private fun handle(e: Exception): Unit { println("got exception") }
And this how I call it in a test...
fun main(vararg argv: String): Unit {
val callable1 = object : Callable<Int> {
override fun call(): Int = 1
}
val c1 = createCallable(callable1)
println("callable1 = ${c1.call()}")
val callable2 = object : Callable<Unit> {
override fun call(): Unit { println("Hello"); throw Exception("Hello") }
}
val c2 = createCallable(callable2)
c2.call()
}