# coroutines

Marty Pitt

08/23/2021, 8:14 AM
Hi. I'd like a review / feedback of an approach I'm using, joining two libraries outside of our control - one that uses CompletableFuture<>, and one that uses
suspend fun
We have some code that is being invoked by a 3rd party library (Hazelcast Jet), which is going to call a service defined as
suspend fun ...
These calls will happen fairly frequently. The service call is defined as suspend becuase it has I/O. We can't really change either interfaces here. In my code that will invoke the
fun, I need to return a CompletableFuture containing my result, so I'm using the
library with
CoroutineScope.future { }
extension. The hard part (for me) has been working out how to get a scope to call the .future method against. I don't want to use
, but I find the documentation confusing for how to handle this scenario.
This is what I've got so far, but would appreciate feedback:
Copy code
 * A coroutine scope that defers work back to the Unconfined dispatcher.
 * This means that work that starts uses the calling thread until such time as it
 * hits a suspension point, when it defers to that suspension code.
 * Since the Transformation job is running in a thread from a CompletableFuture threadpool,
 * we want to attach to the calling thread, until the code defers.
 * This seems like a reasonable approach, but need validation.
internal class TransformationScope : CoroutineScope {
   override val coroutineContext: CoroutineContext
      get() = Dispatchers.Unconfined
Then, in my calling code:
Copy code
val future: CompletableFuture<MyResult> = TransformationScope().future {
   .. do work 
Does this look correct? Is it appropriate that we create a new scope for each unit of work? Do I need to implement / consider cancellation here? (There's no concept of cancellation from the caller)

Nick Allen

08/24/2021, 2:54 AM
You code is equivalent to
GlobalScope.future(Dispatchers.Unconfined) { ... }
. So you are not helping yourself at all. The reason
is frowned upon is that it's often used incorrectly. Usually work is done in the context of some larger group of work, and structured concurrency gives the benefits of being able to cancel all that work, fail the entire group if part fails, or wait for all that work to be done. Structured concurrency only works if the CoroutineScope has a
in the context to represent that larger group of work.
does not have a
, not does your
. So each launched coroutine is completely independent. Being completely independent is perfect for some cases, like a daemon coroutine that lives for the whole process, or if you need to manage the coroutine explicitly for some reason. But usually structured concurrency is what you really want because the coroutine is part of some larger task or lifecycle. Your use case kinda seems like it may fall under the "manage the coroutine explicitly" case if the 3rd party library is in charge of the lifetime. And yes, the new scope each time, is pointless. You could also go with an object like
object TransformationScope : CoroutineScope by (GlobalScope + Dispatchers.Unconfined)
or just
val transformationScope = GlobalScope + Dispatchers.Unconfined

Marty Pitt

08/24/2021, 3:45 PM
Thanks @Nick Allen!