Here's a story about a couple of nasty bugs I ran ...
# feed
s
Here's a story about a couple of nasty bugs I ran into when adapting a Caffeine
AsyncCache
for use with Kotlin coroutines. One of the bugs is with coroutine context being leaked between coroutines, and the other is related to cancellation. Hope it can help others avoid the same issues in future! https://sam-cooper.medium.com/0b6e9fecad11
p
Have you come across https://github.com/sksamuel/aedile ?
s
Oh, interesting! I hadn't seen that one. I did see https://github.com/be-hase/caffeine-coroutines which is linked from the Caffeine GitHub repo.
Looks like both libraries run into both of the issues I described by default 馃槥 although aedile does let you swap out the scope if you want
Thanks for the link! Maybe I should start filing some GitHub issues to see what the library maintainers think, or if there's things I'm missing
w
I have my own wrapper for caches that gives them a hot flow's
WhileSubscribed
semantics. Such that it's canceled as soon as there is no listeners for that value anymore. Perhaps these caching libraries should provide this out of the box? :)
And @Sam, great read! I really like that you are pointing people at some common mistakes with Coroutines. It's indeed a big and common problem that people leak their `CoroutineScope`s out of the
coroutineScope { }
block. I was surprised to find no link between this article and https://sam-cooper.medium.com/the-silent-killer-thats-crashing-your-coroutines-9171d1e8f79b. Because leaking `CoroutineScope`s is exactly what leads to cancellation exceptions leaking into other coroutine trees, which leads to the necessity of
ensureActive()
instead of rethrowing. I have one additional tip though. I urge everyone who makes suspending higher order functions to give them a
CoroutineScope
receiver. There are many footguns that occur when it's not there. Now you could still accidentally leak cancellation as follows:
Copy code
someFlow().collectLatest {
    coroutineScope {
        cache.getOrPut("foo") { async { computationB() }.let { computationA() + it.await() }
    }
}
Here if the computation get's canceled and restarted because of an emission into the flow, then the cancellation exception from the previous emission gets thrown into the next. `kotlinx.coroutine`'s
collectLatest
is missing the receiver as well, and <http://[`Flow.collectLatest`%20is%20error-prone%20because%20of%20no%20`CoroutineScope`%20路%20Issue%20#3533%20路%20Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines/issues/3533)|we regret this>.
馃檱 1
s
The
WhileSubscribed
solution is a really great one very nice I think I've used that before, though I'd completely forgotten about it until you mentioned it. Might have to write an article on that too!
馃敟 1
I'm really glad you liked the article. I did include a link to the cancellation article right at the end, I think, but I decided not to go into it too much. It could probably be a whole article on its own, there's a lot of nuance
馃憤 1