spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.64k stars 38.14k forks source link

@DirtiesContext, @Async and @EnableAsync(mode = AdviceMode.ASPECTJ) #30229

Closed kicktipp closed 1 year ago

kicktipp commented 1 year ago

Affects: \<6.0.6>


This is my AsyncConfig with Spring Boot. We use Compile Time Weaving with spring-aspects


@Configuration
@EnableAsync(mode = AdviceMode.ASPECTJ)
public class AsyncConfig implements AsyncConfigurer {
    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setThreadNamePrefix("AsyncExecutor-");
        threadPoolTaskExecutor.setCorePoolSize(15);
        threadPoolTaskExecutor.setAwaitTerminationSeconds(10);
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }

    @Bean
    public Executor taskExecutor() {
        var delegatingSecurityContextAsyncTaskExecutor = new DelegatingSecurityContextAsyncTaskExecutor(threadPoolTaskExecutor());
        return delegatingSecurityContextAsyncTaskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncExceptionHandler();
    }
}

When I have a test like this:

    @Test
    @DirtiesContext
    public void test1() {
          myservice.doSomethingWithAsync()
    }

    @Test
    @DirtiesContext
    public void test2() {
          myservice.doSomethingWithAsync()
    }

I get an error in the second test:

2023-03-29T11:50:09.788+02:00 ERROR 72798 --- [o-auto-6-exec-1] de.kicktipp.core.mailutils.MailHelper : Executor [java.util.concurrent.ThreadPoolExecutor@1294ca02[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 13]] did not accept task: org.springframework.scheduling.aspectj.AbstractAsyncExecutionAspect$AbstractAsyncExecutionAspect$1@2283d58f

Debugging it I found that AsyncExecutionAspectSupport holds a reference to executors in line 76:

private final Map<Method, AsyncTaskExecutor> executors = new ConcurrentHashMap<>(16);

This AsyncExecutionAspectSupport is not reloaded when the context is reloaded. It holds a reference to the old executor from the first context. This executor is of course terminated and should not be used.

Workaround:

import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.aspectj.AnnotationAsyncExecutionAspect;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Map;

@Component
public class DirtiesContextWithAsyncAspectJFix implements DisposableBean {

    @Override
    public void destroy() {
        try {
            var asyncSupport = AnnotationAsyncExecutionAspect.aspectOf();
            var field = ReflectionUtils.findField(asyncSupport.getClass(), "executors");
            assert field != null;
            field.setAccessible(true);
            @SuppressWarnings("unchecked")
            var executors = (Map<Method, AsyncTaskExecutor>) field.get(asyncSupport);
            executors.clear();
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

AsyncExecutionAspectSupport should reset its executors cache on context shutdown.

snicoll commented 1 year ago

Thanks for the report. that's yet another incarnation of #11019

The issues are linked now so we'll look at that particular use case when we deal with the issue.