Search code examples
javaspringspring-boottomcat9thread-local

Spring 5.x - How to cleanup a ThreadLocal entry


Apologies for the long question..

I'm fairly new to Spring and don't understand the inner working fully yet.

So, my current java project has Spring 4.x code written way back in 2015 that uses ThreadLocal variable to store some user permission data.

The flow starts as a REST call in a REST controller which then calls the backend code and checks for user permissions from the DB.

There is a @Repository class that has a static instance of ThreadLocal where this user permission is stored. The ThreadLocal variable is updated by the calling thread. So, if the thread finds data in the ThreadLocal instance already present for it, it just reads that data from the ThreadLocal variable and works away. If not, it goes to DB tables and fetches new permission data and also updates the ThreadLocal variable.

So my understanding is that ThreadLocal variable was used as these user permissions are needed multiple times within the same REST Call. So the idea was for a given REST request since the thread is the same, it needn't fetch user permissions from DB and instead can refer to its entry in the ThreadLocal variable within the same REST request.

Now, this seems to work fine in Spring 4.3.29.RELEASE as every REST call was being serviced by a different thread.(I printed Thread IDs to confirm.)

Spring 4.x ThreadStack up to Controller method call:

com.xxx.myRESTController.getDoc(MyRESTController.java),
org.springframework.web.context.request.async.WebAsyncManager$5.run(WebAsyncManager.java:332),
java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511),
java.util.concurrent.FutureTask.run(FutureTask.java:266),
java.lang.Thread.run(Thread.java:748)]

However, when I upgraded to Spring 5.2.15.RELEASE this breaks when calling different REST endpoints that try to fetch user permissions from the backend.

On printing the Stacktrace in the backend, I see there is a ThreadPoolExecutor being used in Spring 5.x.

Spring 5.x ThreadStack:

com.xxx.myRESTController.getDoc(MyRESTController.java),
org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:337),
java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511),
java.util.concurrent.FutureTask.run(FutureTask.java:266),
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149),
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624),
java.lang.Thread.run(Thread.java:748)]

So in Spring 5.x, it looks like the same thread is being put back in the ThreadPool and later gets called for multiple different REST calls. When this thread looks up the ThreadLocal instance, it finds stale data stored by it for an earlier unrelated REST call. So quite a few of my test cases fail due to stale data permissions being read by it.

I read that calling ThreadLocal's remove() clears the calling thread's entry from the variable (which wasn't implemented at the time).

I wanted to do this in a generic way so that all REST calls call the remove() before the REST Response is sent back.

Now, in order to clear the ThreadLocal entry, I tried

  1. writing an Interceptor by implementing HandlerInterceptor but this didn't work.

  2. I also wrote another Interceptor extending HandlerInterceptorAdapter and calling ThreadLocal's remove() in its afterCompletion().

  3. I then tried implementing ServletRequestListener and called the ThreadLocal's remove() from its requestDestroyed() method.

  4. In addition, I implemented a Filter and called remove() in doFilter() method.

All these 4 implementations failed cos when I printed the Thread IDs in their methods they were the exact same as each other, but different to the Thread ID being printed in RestController method.

So, the Thread calling the REST endpoint is a different thread from those being called by the above 4 classes. So the remove() call in the above classes never clears anything from ThreadLocal variable.

Can someone please provide some pointers on how to clear the ThreadLocal entry for a given thread in a generic way in Spring?


Solution

  • As you noticed, both the HandlerInterceptor and the ServletRequestListener are executed in the original servlet container thread, where the request is received. Since you are doing asynchronous processing, you need a CallableProcessingInterceptor.

    Its preProcess and postProcess methods are executed on the thread where asynchronous processing will take place.

    Therefore you need something like this:

    WebAsyncUtils.getAsyncManager(request)//
                 .registerCallableInterceptor("some_unique_key", new CallableProcessingInterceptor() {
    
                     @Override
                     public <T> void postProcess(NativeWebRequest request, Callable<T> task,
                             Object concurrentResult) throws Exception {
                         // remove the ThreadLocal
                     }
    
                 });
    

    in a method that has access to the ServletRequest and executes in the original servlet container thread, e.g. in a HandlerInterceptor#preHandle method.

    Remark: Instead of registering your own ThreadLocal, you can use Spring's RequestAttributes. Use the static method:

    RequestContextHolder.currentRequestAttributes()
    

    to retrieve the current instance. Under the hood a ThreadLocal is used, but Spring takes care of setting it and removing it on every thread where the processing of your request takes place (asynchronous processing included).