Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Command Line App hangs after Async method calls complete

I have a Spring Boot Application that uses CommandLineRunner and the Spring @Async annotation to run a method asynchronously. It all works fine, but when all of my threads complete, the application just hangs instead of exiting.

Here is a minimal example of what I have in my application:

Application.java:

@SpringBootApplication
@EnableAsync
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

ApplicationStartup.java:

@Component
public class ApplicationStartup implements CommandLineRunner {

    private final AsyncService asyncService;

    @Inject
    public ApplicationStartup(final AsyncService asyncService) {
        this.asyncService = asyncService;
    }

    @Override
    public void run(final String... strings) throws Exception {
        //my logic is more complicated than this, but this illustrates my point
        for (int i = 0; i < 1000; i++) {
            asyncService.runAsyncMethod();
        }
    }
}

AsyncService.java:

@Service
@Transactional
public class AsyncService {

    @Async
    public void runAsyncMethod() {
        //perform call to an API and process results
    }

}

ExecutorConfig.java:

@Configuration
public class ExecutorConfig() {
    @Bean
    public ThreadPoolTaskExecutor asyncExecutor() {
        final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(64);
        executor.setMaxPoolSize(64);
        executor.setQueueCapacity(500);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setThreadNamePrefix("Scrub-");
        executor.setKeepAliveSeconds(60);
        executor.initialize();
        return executor;
    }
}

All of my threads make the call to runAsyncMethod() and every method call completes successfully, but then the application just hangs.

I tried changing some of the executor settings around. I didn't have the keepAliveSeconds at first, so I thought adding that would fix it, but it still hung after all threads were complete. I changed corePoolSize to 0, and that made the application exit when it was done, but it only used 1 thread the whole time.

Any ideas as to why the application is not exiting with the configuration above?

like image 554
Andrew Mairose Avatar asked May 22 '26 08:05

Andrew Mairose


2 Answers

Even if the marked as correct answer is valid. This is not full answer.

Without @EnableAsync and without WEB environment .web(WebApplicationType.NONE) the spring boot app automatically stop once started(as there is nothing to do/wait). So even if you don't do apringApp.close() in your app but only app.run(commandLine), the .close() method call automatically.

But once you added @EnableAsync - the behavior changes, as there might be async work, so app is not stopped once started. And if there is not stopping code, the app remain working (hangs).

For fixing this you need to do 2 things:

  • in the run method do wait all async work
  • implicitly call .close()after app started

Sample:

    @EnableAutoConfiguration
    @EnableAsync
    public static class SpringApp extends SpringApplication {

        @Bean
        public TaskExecutor taskExecutor () {
            return new SimpleAsyncTaskExecutor();
        }

        @Autowired
        private Service service;

        @EventListener
        public void handleContextRefresh(ContextRefreshedEvent event){
            CompletableFuture<Void> aggregateFuture = service.doWork();
            // avoid exiting this method before all job complected prevents app from hanging
            aggregateFuture.join();

        }
    }
    public static void main(String[] args) {
        SpringApplicationBuilder app = new SpringApplicationBuilder(SpringApp.class).web(WebApplicationType.NONE);
        app.run()
           .close();  // <--- THIS!
    }
like image 186
msangel Avatar answered May 23 '26 21:05

msangel


You missed to join the asynchronous jobs, that's why the run method exits (far) before all threads complete - and the awkward behavior is "more comprehensible".

According to doc, you could join like:

...
CompletableFuture<Void>[] myJobs = new CompletableFuture<>[N];
...
for (int i = 0; i < N; i++) {
        myJobs[i] = asyncService.runAsyncMethod();
}
...
CompletableFuture.allOf(myJobs).join();

And your runAsyncMethod() would need to return a CompletableFuture<Void>. To do so, you can just return CompletableFuture.completedFuture(null);

like image 25
xerx593 Avatar answered May 23 '26 22:05

xerx593



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!