I have a spring boot application where a users hits an endpoint and I have to acknowledge that I got their request immediately. I need to do some computation on different thread and send them response on a different endpoint after my computation is over. For executing the task on different thread my thread pool configuration looks something like this:
@Configuration
@EnableAsync
public class SpringAsyncMatchingConfig {
@Bean(name = "threadTaskExecutor")
public TaskExecutor getMatchingTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setQueueCapacity(0);
threadPoolTaskExecutor.setMaxPoolSize(15);
return threadPoolTaskExecutor;
}
}
While I do the computation I need to hit one of the endpoint which returns me a token id and does some computation. It usually takes 3 to 4 minutes to do the computation. So what I have done is I have mentioned Thread.sleep(30000)
and after 30 seconds is completed I again hit the same api with the token id it provided expecting it to give me a result.
while(result == false) {
Thread.sleep(30000)
result = callendpoint(tokenid)
}
Suppose my thread pool is exhausted, it reached its maximum size of 15 threads, and some more tasks are provided to the pool, some of my threads will be in 30 seconds sleep state, will those threads be terminated and assigned a new task because I am in sleep (idle) state? Should I add the set keep alive to prevent this from happening?
threadPoolTaskExecutor.setKeepAliveSeconds(120);
Is this the right thing to do?
Suppose my thread is exhausted it reached maximum size of 15 and some more tasks are provided to the thread, some of my thread will be 30 seconds sleep state
This can happen. Such queue is implemented using LinkedBlockingQueue
and the behavior is as follows (source Spring Framework 5.3.X reference documentation: 7.4.2. The executor
Element):
queueCapacity
is reached then the executor creates a new thread beyond the corePoolSize
up to maxPoolSize
.queueCapacity
is also reached with maxPoolSize
number of threads, the task is rejected.The default capacity is unlimited (in fact Integer.MAX_VALUE
) which means only corePoolSize
number threads will be allocated. If you want to gap the queue capacity, remember to set maxPoolSize
a bit higher than corePoolSize
. To ensure no task will be rejected, you have to find a suitable balance of the number of core
and max
threads. Of course all these numbers heavily depend on the expected throughput. With an "unlimited" queue capacity, you don't need to worry about that, however, the performance-tuning is a bit limited as well.
threadPoolTaskExecutor.setKeepAliveSeconds(120);
Is this the right thing to do?
I don't think so. Speaking to the snippet above this one, note that Thread.sleep(30000)
with such long time doesn't help you with effective result polling which can be handled thorugh the CompletableFuture
instead. Use CompletableFuture::get(long timeout, TimeUnit unit)
to stop the thread when a result is not available after 5 minutes. See below what you can do:
@Component
public class AsyncClient {
@Async
public CompletableFuture<Result> fetchResult(String tokenId) {
Result result = callEndpoint(tokenId); // a long call
return CompletableFuture.completedFuture(results);
}
}
@Service
public class TokenService {
private final AsyncClient asyncClient;
public Result callEndpoint(String tokenId)
throws InterruptedException, ExecutionException, TimeoutException
{
CompletableFuture<Result> completableFuture = asyncClient.fetchResult(tokenId);
return completableFuture.get(5, TimeUnit.SECONDS);
}
}
Finally, let me also remind you three basic rules of using @Async
:
public
methods onlyvoid
(more suitable for jobs) or Future