Kotlin 非同期 スレッドローカルの値参照

非同期処理を実装したところ、 IllegalStateException がスローされて対応に四苦八苦したのでそのメモ
実行環境
Kotlin
Spring Boot

IllegalStateException の全文
No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
DeepLで翻訳かけた結果
スレッドバインドされたリクエストは見つかりませんでした。実際のWebリクエストの外でのリクエスト属性を指しているのでしょうか、それとももともと受信していたスレッドの外でリクエストを処理しているのでしょうか?もし、実際にWebリクエスト内で操作しているにも関わらず、このメッセージが表示される場合は、あなたのコードがDispatcherServletの外部で実行されている可能性があります。この場合、RequestContextListenerやRequestContextFilterを使って、現在のリクエストを公開してください。
エラーをスローした処理を特定してみた
RequestContextHolder の currentRequestAttributes で呼ばれていることが判明
非同期処理中に currentRequestAttributes メソッドを経由して RequestAttributesを参照しているのが、この値が非同期処理で生成された別スレッドにはないためエラーになる。

解決方法(NG)
Executor インターフェイスのメソッドだけオーバーライドしRunnable インターフェイスも別途自前で実装。
実装したRunnable インターフェイスに RequestAttributes をセットしてあげれば非同期処理でも RequestAttributes を参照することができる

トラブル
Javaだと問題ないがKotlin で実装する場合、 AsyncConfigurer を実装しようとしたら起動できない
Failed to instantiate [java.util.concurrent.Executor]: Illegal arguments to factory method 'getAsyncExecutor'; args: ; nested exception is java.lang.IllegalArgumentException: object is not an instance of declaring class
調べてみると同様の issue が上がっており、 Spring Cloud Sleuth のバグっぽいらしい

回避策
紐付いている issue を読んでみると回避策が提示されていた
@Configuration
@EnableAsync
class AsyncConfiguration {
@Bean
fun asyncConfigurer(): AsyncConfigurer {
return object : AsyncConfigurer {
override fun getAsyncExecutor(): Executor {
val executor = ThreadPoolTaskExecutor()
executor.initialize()
return executor
}
override fun getAsyncUncaughtExceptionHandler(): AsyncUncaughtExceptionHandler {
return YourCustomAsyncUncaughtExceptionHandler()
}
}
}
}

解決
AsyncConfigurer の実装方法がわかった + 非同期処理で RequestAttributes を参照できるようにするコード
@Configuration
@EnableAsync
class AsyncConfiguration {
@Bean
fun asyncConfigurer(): AsyncConfigurer {
return object : AsyncConfigurer {
override fun getAsyncExecutor(): Executor {
val executor = ContextAwarePoolExecutor()
// executor のパラメータは割愛
executor.initialize()
return executor
}
}
}
}
class ContextAwarePoolExecutor : ThreadPoolTaskExecutor() {
override fun execute(task: Runnable) {
super.execute(ContextAwareRunnable(task, RequestContextHolder.currentRequestAttributes()))
}
class ContextAwareRunnable(
private val task: Runnable,
private val context: RequestAttributes?
) : Runnable {
override fun run() {
if (context != null) {
RequestContextHolder.setRequestAttributes(context)
}
try {
task.run()
} finally {
RequestContextHolder.resetRequestAttributes()
}
}
}
}