article banner

Exercise: raceOf

You implement an application that can take data from two sources and should always choose the faster response. For that, you decide to implement a raceOf function that starts two or more asynchronous computations and returns the result of the first one that completes. This is an example of how it could work:

suspend fun fetchUserData(): UserData = raceOf( { service1.fetchUserData() }, { service2.fetchUserData() } )

Implement a raceOf function that takes a list of suspending functions and returns the result of the first one that completes. Use the select expression to implement it.

suspend fun <T> raceOf( racer: suspend CoroutineScope.() -> T, vararg racers: suspend CoroutineScope.() -> T ): T = TODO()

This problem can either be solved in the below playground or you can clone kotlin-exercises project and solve it locally. In the project, you can find code template for this exercise in coroutines/recipes/RaceOf.kt. You can find there starting code and unit tests.

Once you are done with the exercise, you can check your solution here.

Playground

import kotlinx.coroutines.* import kotlinx.coroutines.selects.select import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import kotlin.coroutines.CoroutineContext import org.junit.Test import kotlin.test.assertEquals suspend fun <T> raceOf( racer: suspend CoroutineScope.() -> T, vararg racers: suspend CoroutineScope.() -> T ): T = TODO() class RaceOfTest { @Test fun should_wait_for_the_fastest() = runTest { raceOf( { delay(3) }, { delay(1) }, { delay(2) }, ) assertEquals(1, currentTime) } @Test fun should_wait_for_the_fastest_for_big_number() = runTest { val racers = List<suspend CoroutineScope.() -> Long>(1000) { i -> { val num = (i + 100).toLong() delay(num) num } }.shuffled().toMutableList() val result = raceOf(racers.removeFirst(), *racers.toTypedArray()) assertEquals(100, result) assertEquals(100, currentTime) } @Test fun should_respond_with_fastest() = runTest { val result = raceOf( { delay(3000); "C" }, { delay(1000); "A" }, { delay(2000); "B" }, ) assertEquals("A", result) assertEquals(1000, currentTime) } @Test fun should_cancel_slower() = runTest { var slowerJob: Job? = null val result = raceOf( { delay(1000); "A" }, { slowerJob = currentCoroutineContext().job; delay(2000); "B" }, ) assertEquals("A", result) assertEquals(1000, currentTime) assertEquals(true, slowerJob?.isCancelled) } @Test fun should_cancel_when_parent_cancelled() = runTest { var innerJob: Job? = null val job = launch { raceOf( { delay(1000) }, { innerJob = currentCoroutineContext().job; delay(2000) }, ) } delay(500) assertEquals(true, innerJob?.isActive) job.cancel() assertEquals(true, innerJob?.isCancelled) } @Test fun should_propagate_context() = runTest { var innerCtx: CoroutineContext? = null val coroutineName1 = CoroutineName("ABC") withContext(coroutineName1) { raceOf( { delay(1000) }, { innerCtx = currentCoroutineContext(); delay(2000) }, ) } delay(500) assertEquals(coroutineName1, innerCtx?.get(CoroutineName)) val coroutineName2 = CoroutineName("DEF") withContext(coroutineName2) { raceOf( { delay(1000) }, { innerCtx = currentCoroutineContext(); delay(2000) }, ) } delay(500) assertEquals(coroutineName2, innerCtx?.get(CoroutineName)) } }