ing-bank/baker

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 the Deferred 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.