springspring-bootspring-restcontrollerspring-asyncrequest-timed-out

Best way to limit time execution in a @RestController


Considering the following code:

@RestController
@RequestMapping("/timeout")
public class TestController {

    @Autowired
    private TestService service;

    @GetMapping("/max10secs")
    public String max10secs() {
        //In some cases it can take more than 10 seconds
        return service.call();
    }
}

@Service
public class TestService {

    public String call() {
        //some business logic here
        return response;
    }
}

What I want to accomplish is that if the method call from the TestService takes more than 10 seconds I want to cancel it and generate a response with a HttpStatus.REQUEST_TIMEOUT code.


Solution

  • What I managed to do, but I don't know if there are any conceptual or practical flaws is what it follows...

    First, the configuration of spring-async

    @Configuration
    @EnableAsync
    public class AsyncConfig implements AsyncConfigurer {
    
        @Bean(name = "threadPoolTaskExecutor")
        public Executor threadPoolTaskExecutor() {
    
            ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
            pool.setCorePoolSize(10);
            pool.setMaxPoolSize(10);
            pool.setWaitForTasksToCompleteOnShutdown(true);
            return pool;
        }
    
        @Override
        public Executor getAsyncExecutor() {
            return new SimpleAsyncTaskExecutor();
        }
    }
    

    And next, the Controller and Service modifications:

    @RestController
    @RequestMapping("/timeout")
    public class TestController {
    
        @Autowired
        private TestService service;
    
        @GetMapping("/max10secs")
        public String max10secs() throws InterruptedException, ExecutionException {
            Future<String> futureResponse = service.call();
            try {
                //gives 10 seconds to finish the methods execution
                return futureResponse.get(10, TimeUnit.SECONDS);
            } catch (TimeoutException te) {
                //in case it takes longer we cancel the request and check if the method is not done
                if (futureResponse.cancel(true) || !futureResponse.isDone())
                    throw new TestTimeoutException();
                else {
                    return futureResponse.get();
                }
            }
        }
    }
    
    @Service
    public class TestService {
    
        @Async("threadPoolTaskExecutor")
        public Future<String> call() {
            try{
                //some business logic here
                return new AsyncResult<>(response);
            } catch (Exception e) {
                //some cancel/rollback logic when the request is cancelled
                return null;
            }
        }
    }
    

    And finally generate the TestTimeoutException:

    @ResponseStatus(value = HttpStatus.REQUEST_TIMEOUT, reason = "too much time")
    public class TestTimeoutException extends RuntimeException{ }