Understanding Callable and Spring DeferredResult
1. Introduction
Asynchronous support introduced in Servlet 3.0 offers the possibility to process an HTTP request in another thread. This is specially interesting when you have a long running task, since while another thread processes this request, the container thread is freed and can continue serving other requests.
This topic has been explained many times, but there seems to be a little bit of confusion regarding those classes provided by the Spring framework which take advantage of this functionality. I am talking about returning Callable and DeferredResult from a @Controller.
In this post I will implement both examples in order to show its differences.
All the examples shown here consist on implementing a controller which will execute a long running task, and then return the result to the client. The long running task is processed by the TaskService:
@Service public class TaskServiceImpl implements TaskService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public String execute() { try { Thread.sleep(5000); logger.info("Slow task executed"); return "Task finished"; } catch (InterruptedException e) { throw new RuntimeException(); } } }
The web application is built with Spring Boot. We will be executing the following class to run our examples:
@SpringBootApplication public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class, args); } }
The source code with all these examples can be found at the Github Spring-Rest repository.
2. Starting with a blocking controller
In this example, a request arrives to the controller. The servlet thread won’t be released until the long running method is executed and we exit the @RequestMapping annotated method.
@RestController public class BlockingController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final TaskService taskService; @Autowired public BlockingController(TaskService taskService) { this.taskService = taskService; } @RequestMapping(value = "/block", method = RequestMethod.GET, produces = "text/html") public String executeSlowTask() { logger.info("Request received"); String result = taskService.execute(); logger.info("Servlet thread released"); return result; } }
If we run this example at http://localhost:8080/block, looking at the logs, we can see that the servlet request is not released until the long running task has been processed (5 seconds later):
2015-07-12 12:41:11.849 [nio-8080-exec-6] x.s.web.controller.BlockingController : Request received 2015-07-12 12:41:16.851 [nio-8080-exec-6] x.spring.web.service.TaskServiceImpl : Slow task executed 2015-07-12 12:41:16.851 [nio-8080-exec-6] x.s.web.controller.BlockingController : Servlet thread released
3. Returning Callable
In this example, instead of returning directly the result, we will return a Callable:
@RestController public class AsyncCallableController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final TaskService taskService; @Autowired public AsyncCallableController(TaskService taskService) { this.taskService = taskService; } @RequestMapping(value = "/callable", method = RequestMethod.GET, produces = "text/html") public Callable<String> executeSlowTask() { logger.info("Request received"); Callable<String> callable = taskService::execute; logger.info("Servlet thread released"); return callable; } }
Returning Callable implies that Spring MVC will invoke the task defined in the Callable in a different thread. Spring will manage this thread by using a TaskExecutor. Before waiting for the long task to finish, the servlet thread will be released.
Let’s take a look at the logs:
2015-07-12 13:07:07.012 [nio-8080-exec-5] x.s.w.c.AsyncCallableController : Request received 2015-07-12 13:07:07.013 [nio-8080-exec-5] x.s.w.c.AsyncCallableController : Servlet thread released 2015-07-12 13:07:12.014 [ MvcAsync2] x.spring.web.service.TaskServiceImpl : Slow task executed
You can see that we have returned from the servlet before the long running task has finished executing. This doesn’t mean the client has received a response. The communication with the client is still open waiting for the result, but the thread that received the request has been released and can serve another client’s request.
4. Returning DeferredResult
First, we need to create a DeferredResult object. This object will be returned by the controller. What we will accomplish is the same with Callable, to release the servlet thread while we process the long running task in another thread.
@RestController public class AsyncDeferredController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final TaskService taskService; @Autowired public AsyncDeferredController(TaskService taskService) { this.taskService = taskService; } @RequestMapping(value = "/deferred", method = RequestMethod.GET, produces = "text/html") public DeferredResult<String> executeSlowTask() { logger.info("Request received"); DeferredResult<String> deferredResult = new DeferredResult<>(); CompletableFuture.supplyAsync(taskService::execute) .whenCompleteAsync((result, throwable) -> deferredResult.setResult(result)); logger.info("Servlet thread released"); return deferredResult; }
So, what’s the difference from Callable? The difference is this time the thread is managed by us. It is our responsibility to set the result of the DeferredResult in a different thread.
What we have done in this example, is to create an asynchronous task with CompletableFuture. This will create a new thread where our long running task will be executed. Is in this thread where we will set the result.
From which pool are we retrieving this new thread? By default, the supplyAsync method in CompletableFuture will run the task in the ForkJoin pool. If you want to use a different thread pool, you can pass an executor to the supplyAsync method:
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
If we run this example, we will get the same result as with Callable:
2015-07-12 13:28:08.433 [io-8080-exec-10] x.s.w.c.AsyncDeferredController : Request received 2015-07-12 13:28:08.475 [io-8080-exec-10] x.s.w.c.AsyncDeferredController : Servlet thread released 2015-07-12 13:28:13.469 [onPool-worker-1] x.spring.web.service.TaskServiceImpl : Slow task executed
5. Conclusion
At a high level view, Callable and DeferredResult do the same exact thing, which is releasing the container thread and processing the long running task asynchronously in another thread. The difference is in who manages the thread executing the task.
I’m publishing my new posts on Google plus and Twitter. Follow me if you want to be updated with new content.
Reference: | Understanding Callable and Spring DeferredResult from our JCG partner Xavier Padro at the Xavier Padró’s Blog blog. |
how do i pass arguments with execute method (couldnt call with :: )
Instead of this:
CompletableFuture.supplyAsync(taskService::execute)
.whenCompleteAsync((result, throwable) -> deferredResult.setResult(result));
Use this:
CompletableFuture.supplyAsync(() ->{return taskService.execute();})
.whenCompleteAsync((result, throwable) -> deferredResult.setResult(result));
Just use this- () ->{return taskService.execute();}
Thanks Xavier!
The best explanation I saw on this topic
May be you know how Spring WebFlux implementation for this scenario will look like?
What do you mean by this – “The communication with the client is still open waiting for the result, but the thread that received the request has been released and can serve another client’s request.” What is the benefit of releasing the thread with receive the request? After all the connection is open. I think in the end it doesn’t matter a thread is released it is just delegating its job to another thread. But the API response will still be stuck till another thread will return the result. It’s just like opening another thread and waiting for it to… Read more »
Yes, this make no sense to me if it’s used to just release the controller thread to process another incoming request. The thing is that, to me, i want to response the result to the client and close the communication while another thread is in processing the task, for the result of this thread’s task doesn’t require to response to client.
Is there anyway we could achieve it?