Zenn
Closed6

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

yocyoc

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

実行環境
Kotlin
Spring Boot

yocyoc

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 で呼ばれていることが判明
https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/request/RequestContextHolder.html

非同期処理中に currentRequestAttributes メソッドを経由して RequestAttributesを参照しているのが、この値が非同期処理で生成された別スレッドにはないためエラーになる。

yocyoc

解決方法(NG)

https://lowreal.net/2018/11/15/1

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

yocyoc

トラブル

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 のバグっぽいらしい
https://github.com/spring-projects/spring-boot/issues/29913

yocyoc

回避策

紐付いている issue を読んでみると回避策が提示されていた
https://github.com/spring-cloud/spring-cloud-sleuth/issues/2100#ref-issue-1218761407

@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()
            }
        }
    }
}
yocyoc

解決

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()
            }
        }
    }
}

このスクラップは2023/11/15にクローズされました
ログインするとコメントできます