Hi. I'd like a review / feedback of an approach I...
# coroutines
m
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
suspend
fun, I need to return a CompletableFuture containing my result, so I'm using the
kotlinx-coroutines-jdk8
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
GlobalScope
, 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)
n
You code is equivalent to
GlobalScope.future(Dispatchers.Unconfined) { ... }
. So you are not helping yourself at all. The reason
GlobalScope
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
Job
in the context to represent that larger group of work.
GlobalScope
does not have a
Job
, not does your
TransformationScope()
. 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
m
Thanks @Nick Allen!