smallrye-mutiny icon indicating copy to clipboard operation
smallrye-mutiny copied to clipboard

New feature: provide a `uni { }` coroutine builder

Open rgmz opened this issue 3 years ago • 3 comments

Context

(Motivated by https://github.com/quarkusio/quarkus/issues/21297)

The existing mutiny-kotlin module provides extensions methods for converting between Uni/Mutli and coroutines; however, it's missing a straightforward way to return a suspending function as a Uni.

The example in the documentation accomplishes this by using async:

val deferred: Deferred<String> = GlobalScope.async { "Kotlin ❤ Mutiny" }
val uni: Uni<String> = deferred.asUni()

To accomplish this for normal suspend methods, you'd need to either unnecessarily wrap it async { } , or create your own emitter block:

// You either need to wrap it as `async`, which is awkward...
suspend fun returnsUni1(): Uni<String> = async { getFoo() }.asUni()
// Or manually create the uni from `emitter`, like what's done for `Deferred<T>.asUni()`
suspend fun returnsUni2(): Uni<String> = Uni.createFrom().emitter { em: UniEmitter<in T> -> 
     invokeOnCompletion { 
         try { 
             em.complete(getFoo()) 
         } catch (th: Throwable) { 
             em.fail(th) 
         } 
     } 
 }

suspend fun getFoo(): String {
  delay(500)
  "Foo"
}

Description

It would use useful to have a uni { } coroutine builder that lets you return a Uni<T> from the execution of a coroutine:

suspend fun returnsUni(): Uni<String> = uni { getFoo() }

The implementation would be similar to Deferred<T>.asUni(), except it would return the result of a block instead of calling getCompleted:

fun <T> uni(block: suspend () -> T): Uni<T> = Uni.createFrom().emitter { em: UniEmitter<in T> -> 
     invokeOnCompletion { 
         try { 
             em.complete(getCompleted()) 
         } catch (th: Throwable) { 
             em.fail(th) 
         } 
     } 
 } 

Additional details

Use of GlobalScope

The Deferred<T>.asUni() example uses GlobalScope, which is generally discouraged from use). In the context of Quarkus you'd presumably want this to run on a Vert.x context.

Using uni { } outside of a coroutine scope

In my experience, it would also be useful to for the uni { } builder to support calling a suspending function from a non-suspending function. Sometimes you need to bridge parts of the codebase and would prefer to use coroutines:

fun existingNonSuspendMethod(): Uni<String> = uni { coGetFoo() }

There's a mono { } coroutine builder for Project Reactor, which makes it easy to return a coroutine as a Mono<T> AND launch a coroutine from outside a CoroutineScope:

fun returnsMono(): Mono<String> = mono {
    coGetFoo()
}

rgmz avatar Nov 15 '21 00:11 rgmz

Thanks for raising the idea @rgmz. Purpose of this library is for sure not, to bridge between "regular" and coroutine code, but I really like the idea of having a uni builder for sake of usability.

Regarding the documentation, you're right. It was just the shortest code snippet - but won't hurt to add a paragraph about how to properly bridge in a Quarkus app using Vertx.dispatcher().

heubeck avatar Nov 15 '21 07:11 heubeck

Purpose of this library is for sure not, to bridge between "regular" and coroutine code, but I really like the idea of having a uni builder for sake of usability.

Agreed; my initial motivation was creating uni { } as a regular coroutine builder, but I got a bit too excited thinking about the edge case. :)

I've updated the original issue, and will open a PR to address the main use case.

rgmz avatar Nov 16 '21 19:11 rgmz

This is a bit trickier than anticipated. Coroutines launched inside Uni.createFrom().emitter won't run, presumably because the block returns immediately.

Most of the "official" extensions under kotlinx.coroutines implement AbstractCoroutine<T>, which is an InternalCoroutinesApi and likely shouldn't be used. One option is to try and get official "mutiny" kotlinx.coroutines bindings. Another option is using GlobalScope: it works, with the caveat that it breaks structured concurrency and we'd have to explicitly manage the job.

I pushed a rough example, if you're curious: https://github.com/rgmz/smallrye-mutiny/tree/feat/kotlin-uni-builder

rgmz avatar Nov 29 '21 14:11 rgmz

Hey @rgmz,

you had to wait long, but now it's here: https://smallrye.io/smallrye-mutiny/2.0.0/guides/kotlin/#uni-builder

Please share your experience and further ideas ;)

heubeck avatar Nov 28 '22 10:11 heubeck