Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 213
Quanta Avatar asked Aug 12 '20 08:08

Quanta


Video Answer


1 Answers

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()

    }

like image 160
Clyde Avatar answered Oct 20 '22 15:10

Clyde