Search code examples
javakotlingoogle-cloud-firestoreconcurrencykotlin-coroutines

Await Java 5 Futures in Kotlin Coroutines without blocking the Thread


I have a suspend function from which I want to return the result of a Java 5 Future. The future object comes from another library Firebase Cloud Firestore- Admin SDK for Java and provides a blocking call get() to retrieve the result of the said future.

My function looks like this-

suspend fun getPrefix(messageCreateEvent: MessageCreateEvent): String {

    val snapshot = db.collection("prefixes")
        .document(messageCreateEvent.guildId.get().asString())
        .get() //This returns a future
        .get() //Retrieves the future's result (Blocks thread; IDE gives warning)

    //Return the prefix
    return if (snapshot.exists())
        snapshot.getString("prefix") ?: DEFAULT_PREFIX
    else DEFAULT_PREFIX
}

Solutions I have considered

The first thing that I considered was to look in kotlinx.coroutine for extensions to bridge the futures. While the extensions exist, they do only for CompletionStatge. So I decided to wrap the future into one ()-

val snapshot = CompleteableFuture.supplyAsync {
    db.collection("prefixes")
        .document(messageCreateEvent.guildId.get().asString())
        .get() // This returns a future
        .get() // Get the result
}.await()

I am quite inexperienced and not sure if this is was proper solution. I queried my question on a programming community, where a person recommended me to use a Deferred-

val deferred = CompletableDeferred<DocumentSnapshot>()
val future = db.collection("prefixes")
    .document(messageCreateEvent.guildId.get().asString())
    .get()

future.addListener(
    Runnable { deferred.complete(future.get()) },
    ForkJoinPool.commonPool()
)
            
val snapshot = deferred.await()

I've give it quite a time to search for a way to bridge futures to co-routines, there isn't even a similar question on SO. Through, I wouldn't be surprised if this question gets a duplicate mark.


Solution

  • The key to this problem is the suspendCoroutine function. The other non-obvious bit is that to add a callback to the ApiFuture you use a static method on ApiFutures.

    Here's an extension function that implements await() on an ApiFuture.

            /**
             * Function to convert an ApiFuture into a coroutine return.
             */
            suspend fun <F : Any?, R : Any?> ApiFuture<F>.await(
                successHandler: (F) -> R,
            ): R {
                return suspendCoroutine { cont ->
                    ApiFutures.addCallback(this, object : ApiFutureCallback<F> {
                        override fun onFailure(t: Throwable?) {
                            cont.resumeWithException(t ?: IOException("Unknown error"))
                        }
    
                        override fun onSuccess(result: F) {
                            cont.resume(successHandler(result))
                        }
                    }, Dispatchers.IO.asExecutor())
                }
            }
    
    /**
     * inline function to retrieve a document as a POJO from a DocumentReference
    */
    
    suspend inline fun <reified T: Any>DocumentReference.toObject(): T? {
                return get().await<DocumentSnapshot, T?> {
                    it.toObject(T::class.java)
                }
            }
    

    Now you can write things like:

        suspend fun getUser(id: String): User? {
            return db.collection("users").document(id).toObject()
    
        }