2

I have a controller with WebAsyncTask. Further on I'm using a timeout callback. As writen here I shall have an option to notifies the Callable to cancel processing. However I don't see any option to do so.

@Controller
public class UserDataProviderController {

    private static final Logger log = LoggerFactory.getLogger(UserDataProviderController.class.getName());

    @Autowired
    private Collection<UserDataService> dataServices;

       @RequestMapping(value = "/client/{socialSecurityNumber}", method = RequestMethod.GET)
        public @ResponseBody
        WebAsyncTask<ResponseEntity<CustomDataResponse>> process(@PathVariable final String socialSecurityNumber) {

            final Callable<ResponseEntity<CustomDataResponse>> callable = new Callable<ResponseEntity<CustomDataResponse>>() {

                @Override
                public ResponseEntity<CustomDataResponse> call() throws Exception {

                    CustomDataResponse CustomDataResponse = CustomDataResponse.newInstance();

                    // Find user data
                    for(UserDataService dataService:dataServices)
                    {
                        List<? extends DataClient> clients = dataService.findBySsn(socialSecurityNumber);
                        CustomDataResponse.put(dataService.getDataSource(), UserDataConverter.convert(clients));
                    }

                    // test long execution
                    Thread.sleep(4000);

                    log.info("Execution thread continued and shall be terminated:"+Thread.currentThread().getName());


                    HttpHeaders responseHeaders = new HttpHeaders();
                    responseHeaders.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
                    return new ResponseEntity(CustomDataResponse,responseHeaders,HttpStatus.OK);
                }

            };

            final Callable<ResponseEntity<CustomDataResponse>> callableTimeout = new Callable<ResponseEntity<CustomDataResponse>>() {
                @Override
                public ResponseEntity<CustomDataResponse> call() throws Exception {

                    // Error response
                    HttpHeaders responseHeaders = new HttpHeaders();
                    responseHeaders.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
                    return new ResponseEntity("Request has timed out!",responseHeaders,HttpStatus.INTERNAL_SERVER_ERROR);
                }
            };

            WebAsyncTask<ResponseEntity<CustomDataResponse>> task = new WebAsyncTask<>(3000,callable);
            task.onTimeout(callableTimeout);
            return task;
        }
}

My @WebConfig

@Configuration
@EnableWebMvc
class WebAppConfig  extends WebMvcConfigurerAdapter {

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setKeepAliveSeconds(60 * 60);
        executor.afterPropertiesSet();

        configurer.registerCallableInterceptors(new TimeoutCallableProcessingInterceptor());
        configurer.setTaskExecutor(executor);
    } 
}

And quite standard Interceptor:

public class TimeoutCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter {

    @Override
    public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) {

        throw new IllegalStateException("[" + task.getClass().getName() + "] timed out");

    }
}

Everything work as it should, but Callable from controller always completes, which is obvious, but how to stop processing there ?

Premek
  • 99
  • 1
  • 6
  • You mean the callable that takes 4 seconds is not stopped after 3 seconds? – fps Mar 18 '15 at 11:01
  • Yes. Basically i don't want any code being executed after sleep. – Premek Mar 18 '15 at 13:29
  • Possible duplicate - http://stackoverflow.com/questions/16101069/spring-mvc-callable-execution-continues-even-after-request-timeout – hemant1900 Mar 18 '15 at 16:21
  • @Premek That's the thing. You have to handle interruption manually, i.e. the callable that returns the result from the data services should check if it has received an interruption signal, and act upon it, in your case, by sending another response. – fps Mar 18 '15 at 17:28
  • @Magnamag Do you mean like call: `Thread.currentThread().interrupt();` in callableTimout and check it in controller, somethink like ` if (Thread.currentThread().isInterrupted()){` I'm not sure that this is the right approach... – Premek Mar 19 '15 at 09:32

1 Answers1

0

You can use WebAsyncTask to implement the timeout control and Thread management to stop the new async thread gracefully.

  1. Implement a Callable to run the process
  2. In this method (that runs in a diferent thread) store the current Thread in a Controller's local variable
  3. Implement another Callable to handle timeout event
  4. In this method retrieve the previously stored Thread and interrupt it calling the interrupt() method.
  5. Also throw a TimeoutException to stop the controller process
  6. In the running process, check if the thread interrupted with Thread.currentThread().isInterrupted(), if so, then rollback the transaction throwing an Exception.

Controller:

public WebAsyncTask<ResponseEntity<BookingFileDTO>> confirm(@RequestBody final BookingConfirmationRQDTO bookingConfirmationRQDTO)
        throws AppException,
        ProductException,
        ConfirmationException,
        BeanValidationException {

    final Long startTimestamp = System.currentTimeMillis();
    // The compiler obligates to define the local variable shared with the callable as final array
    final Thread[] asyncTaskThread = new Thread[1];

    /**
     *  Asynchronous execution of the service's task
     *  Implemented without ThreadPool, we're using Tomcat's ThreadPool
     *  To implement an specific ThreadPool take a look at http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-async-configuration-spring-mvc
     */
    Callable<ResponseEntity<BookingFileDTO>> callableTask = () -> {

        //Stores the thread of the newly started asynchronous task
        asyncTaskThread[0] = Thread.currentThread();

        log.debug("Running saveBookingFile task at `{}`thread", asyncTaskThread[0].getName());
        BookingFileDTO bookingFileDTO = bookingFileService.saveBookingFile(
                bookingConfirmationRQDTO,
                MDC.get(HttpHeader.XB3_TRACE_ID))
                .getValue();
        if (log.isDebugEnabled()) {
            log.debug("The saveBookingFile task took {} ms",
                    System.currentTimeMillis() - startTimestamp);
        }
        return new ResponseEntity<>(bookingFileDTO, HttpStatus.OK);
    };

    /**
     * This method is executed if a timeout occurs
     */
    Callable<ResponseEntity<BookingFileDTO>> callableTimeout = () -> {

        String msg = String.format("Timeout detected at %d ms during confirm operation",
            System.currentTimeMillis() - startTimestamp);
        log.error("Timeout detected at {} ms during confirm operation: informing BookingFileService.", msg);

        // Informs the service that the time has ran out
        asyncTaskThread[0].interrupt();

        // Interrupts the controller call
        throw new TimeoutException(msg);
    };

    WebAsyncTask<ResponseEntity<BookingFileDTO>> webAsyncTask = new WebAsyncTask<>(timeoutMillis, callableTask);
    webAsyncTask.onTimeout(callableTimeout);
    log.debug("Timeout set to {} ms", timeoutMillis);
    return webAsyncTask;
}

Service implementation:

/**
 * If the service has been informed that the time has ran out
 * throws an AsyncRequestTimeoutException to roll-back transactions
 */
private void rollbackOnTimeout() throws TimeoutException {
    if(Thread.currentThread().isInterrupted()) {
        log.error(TIMEOUT_DETECTED_MSG);
        throw new TimeoutException(TIMEOUT_DETECTED_MSG);
    }
}

@Transactional(rollbackFor = TimeoutException.class, propagation = Propagation.REQUIRES_NEW)
DTOSimpleWrapper<BookingFileDTO> saveBookingFile(BookingConfirmationRQDTO bookingConfirmationRQDTO, String traceId) {

    // Database operations
    // ...

    return retValue;
}
p3quod
  • 1,449
  • 3
  • 13
  • 15