|
| 1 | +# 🖥️ Kotlin Coroutine Environments QueryCountPerRequest Usage Guide |
| 2 | + |
| 3 | +**English** | [한국어](README-kotlin-coroutine-environments.md) |
| 4 | + |
| 5 | +## 🖥️ Overview |
| 6 | + |
| 7 | +The Multi-Datasource-Query-Counter library counts the number of DB queries per API request. |
| 8 | +The library basically uses `@RequestScope` to ensure that each API request has its own counter. |
| 9 | +However, in Kotlin Coroutine environments, since Coroutines can freely move between threads (`suspension`, `resumption`), |
| 10 | +an issue may occur where the context of `RequestContextHolder` is not maintained. |
| 11 | + |
| 12 | +This guide explains how to properly use `QueryCountPerRequest` in Coroutine environments. |
| 13 | + |
| 14 | +<br> |
| 15 | + |
| 16 | +## 🖥️ The Problem |
| 17 | + |
| 18 | +Coroutines have the following characteristics: |
| 19 | + |
| 20 | +1. **Suspension, Resumption**: Coroutines can be suspended and resumed at any time. |
| 21 | +2. **Thread Switching**: When a Coroutine resumes, it can resume on a different thread than the original one. |
| 22 | +3. **Context Propagation**: When starting a new Coroutine with builders like `async` or `launch`, the parent Coroutine's context may not be automatically propagated. |
| 23 | + |
| 24 | +Therefore, it is difficult to properly use `QueryCountPerRequest` by default. |
| 25 | + |
| 26 | +<br> |
| 27 | + |
| 28 | +## 🖥️ Solution: Coroutine Context Element |
| 29 | + |
| 30 | +### 1. Using CoroutineQueryCountContextElement |
| 31 | + |
| 32 | +`CoroutineQueryCountContextElement` implements the [`ThreadContextElement` interface](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-thread-context-element/) to propagate `RequestAttributes` to the branching Coroutine contexts. |
| 33 | + |
| 34 | +```kotlin |
| 35 | +class CoroutineQueryCountContextElement( |
| 36 | + private val requestAttributes: RequestAttributes = RequestContextHolder.currentRequestAttributes(), |
| 37 | +) : ThreadContextElement<RequestAttributes> { |
| 38 | + |
| 39 | + companion object Key : CoroutineContext.Key<CoroutineQueryCountContextElement> |
| 40 | + |
| 41 | + override val key: CoroutineContext.Key<CoroutineQueryCountContextElement> |
| 42 | + get() = Key |
| 43 | + |
| 44 | + override fun updateThreadContext(context: CoroutineContext): RequestAttributes { |
| 45 | + RequestContextHolder.setRequestAttributes(requestAttributes) |
| 46 | + return requestAttributes |
| 47 | + } |
| 48 | + |
| 49 | + override fun restoreThreadContext(context: CoroutineContext, oldState: RequestAttributes) { |
| 50 | + RequestContextHolder.setRequestAttributes(oldState) |
| 51 | + } |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +The actual implementation is included in the library, so you can use it immediately. |
| 56 | + |
| 57 | +### 2. Usage Example |
| 58 | + |
| 59 | +```kotlin |
| 60 | +@RestController |
| 61 | +class UserController( |
| 62 | + private val userRepository: UserRepository, |
| 63 | +) { |
| 64 | + @CountQueries |
| 65 | + @GetMapping("/users/coroutine") |
| 66 | + fun getUsers() { |
| 67 | + // Now this Coroutine and its branching Coroutines maintain the same RequestAttributes |
| 68 | + val element = CoroutineQueryCountContextElement() |
| 69 | + val threadPool = ForkJoinPool(2) |
| 70 | + return runBlocking(threadPool.asCoroutineDispatcher() + element) { |
| 71 | + val usersDeferred = async { |
| 72 | + userRepository.findAllUsers() // Query count point. |
| 73 | + } |
| 74 | + |
| 75 | + val countDeferred = async { |
| 76 | + userRepository.count() // Query count point. |
| 77 | + } |
| 78 | + |
| 79 | + val users = usersDeferred.await() |
| 80 | + val count = countDeferred.await() |
| 81 | + |
| 82 | + mapOf("users" to users, "count" to count) |
| 83 | + } |
| 84 | + } |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +<br> |
| 89 | + |
| 90 | +## 🖥️ WebFlux/Reactor Environment |
| 91 | + |
| 92 | +(WIP) |
0 commit comments