Coroutine support
basdgrt opened this issue · 0 comments
At ING we have been talking about adding coroutine support for Baker. I want to use this “issue” to centralize our discussions so anyone in the OSS community can partake.
Rationale
The kotlinx-coroutines-jdk8
bridge makes it easy to integrate coroutines with CompletableFutures
. It's possible for users of Baker to just use the Java Baker in combination with this bridge in Kotlin coroutine-based code. Which looks like this:
fun example() = runBlocking {
baker.bake(recipeId, instanceId).await()
baker.fireEventAndResolveWhenCompleted(instanceId, myEvent).await()
}
This works fine, but it's quite simple to make silly mistakes which might lead to unexpected behaviour.
Forgetting to call await()
It is possible a user accidentally forgets to call await
and ends up with something like this:
fun example() {
baker.bake(recipeId, instanceId)
baker.fireEventAndResolveWhenCompleted(instanceId, myEvent)
}
This results in implicit concurrent execution of both methods. This goes against the grain of idiomatic coroutine usage, as coroutines normally are sequential by default. If you want tasks to execute concurrently you have to be explicit about it by using async
or launch
.
Calling get()
CompletableFuture
exposes a blocking get
method. Calling this method in coroutine-based Kotlin code isn't a good idea.
Exploring options
Due to the fact that suspending functions undergo CPS transformation we can't simply implement the current Baker interface with suspending functions as the function signatures would be incompatible. Hence we investigated some alternative options.
Option 1: Implement Baker interface by returning Deferred
The Baker interface specifies a higer kinded type parameter of F[_]
where F
is the wrapper of the result type. We could create an implementation of this interface with Deferred[_]
. The functions themselves won't be marked as suspending.
Pros:
- The implementations for Scala, Java, and Kotlin Baker adhere to the same interface. Meaning they will always be 'in sync' and the different implementations are easily discoverable.
Cons:
- It's still possible the user forgets to call
await()
. Resulting in implicit concurrent execution of methods. - The
Job
of theDeferred
is not created as a child of the currently running job. This breaks structured concurrency.
Option 2: Create a separate suspending Baker interface
This interface only exposes suspending functions. To implement this interface we could simply delegate the requests to the Java Baker.
Pros:
- Idiomatic solution. This is what most Kotlin engineers will be familiar with.
- If you want to do stuff concurrently you have to be explicit about it.
- Adheres to structured concurrency
Cons:
- Does not adhere to the same interface as the Java and Scala Baker. Meaning they might go 'out of sync' and different implementations are harder to discover.
Option 3: Generate a suspending Baker automatically
Instead of implementing a suspending Baker by hand, we could try to generate it from the Java Baker implementation.
Pros:
- Same as option 2 + the implementations won't go out of sync
Cons:
- Not 100% sure if it's possible to extract the wrapped return types due to Java class type erasure.
- Generating the class somewhere during the build might prove inconvenient.