article banner

Slowing down your code with Coroutines

I was talking about slowing down code and different ways of achieving it and somebody asked me, “why would you want to slow down your code?”. I jokingly replied that my server is running too fast … but in all seriousness, there are legitimate reasons to slow down code.

Let’s have a look at this login endpoint in Chrome Dev Tools where I’m trying to fill in random email addresses with random passwords …

What we see here is that some responses take ~250ms and other responses take ~130ms.

Based on the response times, an attacker can deduce if somebody is signed up for your service or not - a quick response means only a database lookup was done and nothing was found whereas the slower response indicates that the account does exist and extra steps were taken to validate the password.

This type of attack is called a timing attack - A timing attack is a security exploit that allows an attacker to discover vulnerabilities by studying how long it takes the system to respond to different inputs.

Now let’s slow down the login response so it always takes 2 seconds (for a good user-experience, maybe 2 seconds is a bit much, but bear with me here).

I’ve used the exact same inputs as in the previous screenshot, but as you can see, they’re now all saying exactly two seconds making it impossible to distinguish a valid email from an invalid email just by looking at the timings.

Instinctively, my first thought on how to create such a slowdown function was to measure how long the execution took and then delaying the response by the remainder:

suspend fun <T> slowdown(duration: Duration, body: () -> T): T { val (result, timeTaken) = measureTimedValue { body() } delay(duration - timeTaken) return result }

Writing a test case for this, unfortunately, gets tricky since runTest doesn’t play nice with measureTimeMillis. Initially, I had to use runBlocking which meant each test literally took two seconds to run. If you have lots of these endpoints that slow down responses, you will quickly have a mob of grumpy developers with legitimate reasons to slack off:

You’ll also notice that I had to play around with the third parameter in assertEquals which is the tolerance parameter - in other words, if this runs on a slower build server, that tolerance might be too small and the test will randomly fail - not great!

@Test fun `ensure execution takes N seconds when execution faster than N`() = runBlocking { val measuredTime = measureTime { slowdown(2.seconds) { delay(1.seconds) } }.apply(::println) // 2.003796986s assertEquals( 2.seconds.toDouble(DurationUnit.MILLISECONDS), measuredTime.toDouble(DurationUnit.MILLISECONDS), 10.toDouble() ) }

There are several options to solve the slow tests, but all require using runTest and changing the slowdown function so it measures time differently when inside a test.

Option1: we can use context receivers to inject a TimeSource. In normal code, we have to run everything in a TimeSource.Monotonic scope while creating a custom TimeSource for tests. This is about as messy and boilerplate-heavy as it sounds! I’ll leave this as an exercise for the reader to not clutter the article.

context(TimeSource) suspend fun <T> slowdown( duration: Duration, body: suspend () -> T ): T { var result: T val timeTaken = this@TimeSource.measureTime { result = body() } delay(duration - timeTaken) return result }

Option2: we launch a delay in parallel with the body. Although this seems elegant, it is a bit heavy.

suspend fun <T> slowdown( duration: Duration, body: suspend () -> T ): T = coroutineScope { launch { delay(duration) } body() }

Option3: we check what type of dispatcher we’re dealing with and if it’s a TestDispatcher, we measure time differently. This seems like the most optimal solution. (One thing to note, we have to include kotlinx-coroutines-test as a non-test dependency to get access to TestDispatcher)

suspend fun measureCoroutineDuration(body: suspend () -> Unit): Duration { contract { callsInPlace(body, InvocationKind.EXACTLY_ONCE) } val dispatcher = coroutineContext[ContinuationInterceptor] return if (dispatcher is TestDispatcher) { val before = dispatcher.scheduler.currentTime body() val after = dispatcher.scheduler.currentTime after - before } else { measureTimeMillis { body() } }.milliseconds } suspend fun <T> measureCoroutineTimedValue(body: suspend () -> T): TimedValue<T> { contract { callsInPlace(body, InvocationKind.EXACTLY_ONCE) } var value: T val duration = measureCoroutineDuration { value = body() } return TimedValue(value, duration) } suspend fun <T> slowdown(duration: Duration, body: suspend () -> T): T { return measureCoroutineTimedValue(body).also { delay(duration - it.duration) }.value }

For the test cases, we now get to switch to runTest instead of runBlocking which skips delays:

class SlowdownTest { @Test fun `ensure execution takes N seconds when execution faster than N`() = runTest { measureCoroutineDuration { slowdown(2.seconds) { delay(1.seconds) } }.apply(::println).also { assertEquals(2.seconds, it) } // 2s } @Test fun `ensure execution takes more than N seconds when execution slower than N`() = runTest { measureCoroutineDuration { slowdown(1.seconds) { delay(2.seconds) } }.apply(::println).also { assertEquals(2.seconds, it) } // 2s } }

Also we get to check for exactly 2.seconds instead of having to play around with the tolerances which makes the test much more reliable.

Since we’re skipping delays thanks to runTest, we can also expect the tests to finish in milliseconds instead of waiting the full four seconds!

Something as simple as wanting to slow down code turned into a whole article, who would have thought!