Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Async is blocking and prevents nested async calls

Can anybody tell my is there a way of using the Spring Framework's @Async annotation without blocking / waiting on the result? Here is some code to clarify my question:

@Service
public class AsyncServiceA {

    @Autowired
    private AsyncServiceB asyncServiceB;

    @Async
    public CompletableFuture<String> a() {
        ThreadUtil.silentSleep(1000);
        return asyncServiceB.b();
    }
}


@Service
public class AsyncServiceB {

    @Async
    public CompletableFuture<String> b() {
        ThreadUtil.silentSleep(1000);
        return CompletableFuture.completedFuture("Yeah, I come from another thread.");
    }
}

and the configuration:

@SpringBootApplication
@EnableAsync
public class Application implements AsyncConfigurer {

    private static final Log LOG = LogFactory.getLog(Application.class);

    private static final int THREAD_POOL_SIZE = 1;

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

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
            final AsyncServiceA bean = ctx.getBean(AsyncServiceA.class);
            bean.a().whenComplete(LOG::info);
        };
    }

    @Override
    @Bean(destroyMethod = "shutdown")
    public ThreadPoolTaskExecutor getAsyncExecutor() {
        final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(THREAD_POOL_SIZE);
        executor.setMaxPoolSize(THREAD_POOL_SIZE);
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        // omitted
    }
}

When I run the application the executor goes through calling AsyncServiceA.a() and leaves, but it still holds the thread from the pool waiting on the CompletableFuture.get() method. Since there is just a single thread in the pool the AsyncServiceB.b() cannot be executed. What I'm expecting is that thread to be returned to the pool after it executes the AsyncServiceA.a() and then be available to execute the AsyncServiceB.b().

Is there a way to do that?

Note 1: I've tried with ListenableFuture also but the result is the same.

Note 2: I've successfully did it manually (without the @Async) by giving the executor to each method like so:

AsyncServiceA

public CompletableFuture<String> manualA(Executor executor) {
    return CompletableFuture.runAsync(() -> {
        LOG.info("manualA() working...");
        ThreadUtil.silentSleep(1000);
    }, executor)
            .thenCompose(x -> asyncServiceB.manualB(executor));
}

AsyncServiceB

public CompletableFuture<String> manualB(Executor executor) {
    return CompletableFuture.runAsync(() -> {
        LOG.info("manualB() working...");
        ThreadUtil.silentSleep(1000);
    }, executor)
            .thenCompose(x -> CompletableFuture
                    .supplyAsync(() -> "Yeah, I come from another thread.", executor));
}

Here is the ThreadUtil if someone was wondering.

public class ThreadUtil {

    public static void silentSleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Update: Added issue for non-blocking Async annotation https://jira.spring.io/browse/SPR-15401

like image 250
nyxz Avatar asked Mar 29 '17 13:03

nyxz


2 Answers

The @Async support has been part of Spring since Spring 3.0 which was way before the existence of Java8 (or 7 for that matter). Although support has been added for CompletableFutures in later versions it still is to be used for simple async execution of a method call. (The initial implementation reflects/shows the call).

For composing callbacks and operate non blocking the async support was never designed or intended to.

For non blocking support you would want to wait for Spring 5 with its reactive/non-blocking core, next to that you can always submit a ticket for non-blocking support in the async support.

like image 67
M. Deinum Avatar answered Sep 22 '22 06:09

M. Deinum


I've responded on the ticket https://jira.spring.io/browse/SPR-15401 but I'll respond here as well to qualify the response by M. Deinum.

@Async by virtue of how it works (decorating the method call via AOP) can only do one thing, which is to turn the entire method from sync to async. That means the method has to be sync, not a mix of sync and async.

So ServiceA which does some sleeping and then delegates to the async ServiceB would have to wrap the sleeping part in some @Async ServiceC and then compose on ServiceB and C. That way ServiceA becomes async and does not need to have the @Async annotation itself..

like image 24
Rossen Stoyanchev Avatar answered Sep 21 '22 06:09

Rossen Stoyanchev