I have an observable and as it may take a long time i return spring's DeferredResult.
This is the controller logic I'm using:
public DeferredResult<ResponseEntity<InputStreamResource>> getSomeFile() {
DeferredResult<ResponseEntity<InputStreamResource>> deferredResult = new DeferredResult<>(TIMEOUT);
Observable<File> observableFile = fileService.getSomeFile();
observableFile
.map(this::fileToInputStreamResource)
.map(resource -> ResponseEntity.ok().cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()).body(resource))
.subscribe(deferredResult::setResult, ex -> {
deferredResult.setErrorResult(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null));
});
return deferredResult;
}
And this is my test case:
#Test
public void getSomeFile() throws Exception {
this.mockMvc.perform(get("/somefile").accept(MediaType.parseMediaType("application/xml;charset=UTF-8")))
.andExpect(request().asyncStarted())
.andExpect(status().isOk())
.andDo(result -> {
ResponseEntity<InputStreamResource> fileResource = (ResponseEntity<InputStreamResource>) result.getAsyncResult();
InputStream fileResourceInputStream = fileResource.getBody().getInputStream();
FooBar foobar = (FooBar) jaxb2Marshaller.unmarshal(new StreamSource(fileResourceInputStream));
assertThat(foobar.getFoos(), is(not(empty())));
});
}
This test fails when I instantiate DeferredResult without timeout set as it uses generic timeout that is set for rest template (10 sec). If i explicitly set the timeout new DefferredResult(Long.MAX_VALUE) it fails with same exception:
java.lang.IllegalStateException: Async result for handler [public org.springframework.web.context.request.async.DeferredResult<org.springframework.http.ResponseEntity<org.springframework.core.io.InputStreamResource>> com.example.controller.FileController.getSomeFile()] was not set during the specified timeToWait=9223372036854775807
at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:145)
at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:121)
at org.springframework.test.web.servlet.result.RequestResultMatchers$4.match(RequestResultMatchers.java:114)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:171)
So how timeout should be configured or where's a problem ?
Related
I am new to vertx and async programming.
I have 2 verticles communicating via an event bus as follows:
//API Verticle
public class SearchAPIVerticle extends AbstractVerticle {
public static final String GET_USEARCH_DOCS = "get.usearch.docs";
#Autowired
private Integer defaultPort;
private void sendSearchRequest(RoutingContext routingContext) {
final JsonObject requestMessage = routingContext.getBodyAsJson();
final EventBus eventBus = vertx.eventBus();
eventBus.request(GET_USEARCH_DOCS, requestMessage, reply -> {
if (reply.succeeded()) {
Logger.info("Search Result = " + reply.result().body());
routingContext.response()
.putHeader("content-type", "application/json")
.setStatusCode(200)
.end((String) reply.result().body());
} else {
Logger.info("Document Search Request cannot be processed");
routingContext.response()
.setStatusCode(500)
.end();
}
});
}
#Override
public void start() throws Exception {
Logger.info("Starting the Gateway service (Event Sender) verticle");
// Create a Router
Router router = Router.router(vertx);
//Added bodyhandler so we can process json messages via the event bus
router.route().handler(BodyHandler.create());
// Mount the handler for incoming requests
// Find documents
router.post("/api/search/docs/*").handler(this::sendSearchRequest);
// Create an HTTP Server using default options
HttpServer server = vertx.createHttpServer();
// Handle every request using the router
server.requestHandler(router)
//start listening on port 8083
.listen(config().getInteger("http.port", 8083)).onSuccess(msg -> {
Logger.info("*************** Search Gateway Server started on "
+ server.actualPort() + " *************");
});
}
#Override
public void stop(){
//house keeping
}
}
//Below is the target verticle should be making the multiple web client call and merging the responses
.
#Component
public class SolrCloudVerticle extends AbstractVerticle {
public static final String GET_USEARCH_DOCS = "get.usearch.docs";
#Autowired
private SearchRepository searchRepositoryService;
#Override
public void start() throws Exception {
Logger.info("Starting the Solr Cloud Search Service (Event Consumer) verticle");
super.start();
ConfigStoreOptions fileStore = new ConfigStoreOptions().setType("file")
.setConfig(new JsonObject().put("path", "conf/config.json"));
ConfigRetrieverOptions configRetrieverOptions = new ConfigRetrieverOptions()
.addStore(fileStore);
ConfigRetriever configRetriever = ConfigRetriever.create(vertx, configRetrieverOptions);
configRetriever.getConfig(ar -> {
if (ar.succeeded()) {
JsonObject configJson = ar.result();
EventBus eventBus = vertx.eventBus();
eventBus.<JsonObject>consumer(GET_USEARCH_DOCS).handler(getDocumentService(searchRepositoryService, configJson));
Logger.info("Completed search service event processing");
} else {
Logger.error("Failed to retrieve the config");
}
});
}
private Handler<Message<JsonObject>> getDocumentService(SearchRepository searchRepositoryService, JsonObject configJson) {
return requestMessage -> vertx.<String>executeBlocking(future -> {
try {
//I need to incorporate the logic here that adds futures to list and composes the compositefuture
/*
//Below is my logic to populate the future list
WebClient client = WebClient.create(vertx);
List<Future> futureList = new ArrayList<>();
for (Object collection : searchRepositoryService.findAllCollections(configJson).getJsonArray(SOLR_CLOUD_COLLECTION).getList()) {
Future<String> future1 = client.post(8983, "127.0.0.1", "/solr/" + collection + "/query")
.expect(ResponsePredicate.SC_OK)
.sendJsonObject(requestMessage.body())
.map(HttpResponse::bodyAsString).recover(error -> {
System.out.println(error.getMessage());
return Future.succeededFuture();
});
futureList.add(future1);
}
//Below is the CompositeFuture logic, but the logic and construct does not make sense to me. What goes as first and second argument of executeBlocking method
/*CompositeFuture.join(futureList)
.onSuccess(result -> {
result.list().forEach( x -> {
if(x != null){
requestMessage.reply(result.result());
}
}
);
})
.onFailure(error -> {
System.out.println("We should not fail");
})
*/
future.complete("DAO returns a Json String");
} catch (Exception e) {
future.fail(e);
}
}, result -> {
if (result.succeeded()) {
requestMessage.reply(result.result());
} else {
requestMessage.reply(result.cause()
.toString());
}
});
}
}
I was able to use the org.springframework.web.reactive.function.client.WebClient calls to compose my search result from multiple web client calls, as against using Future<io.vertx.ext.web.client.WebClient> with CompositeFuture.
I was trying to avoid mixing Springboot and Vertx, but unfortunately Vertx CompositeFuture did not work here:
//This method supplies the parameter for the future.complete(..) line in getDocumentService(SearchRepository,JsonObject)
private List<JsonObject> findByQueryParamsAndDataSources(SearchRepository searchRepositoryService,
JsonObject configJson,
JsonObject requestMessage)
throws SolrServerException, IOException {
List<JsonObject> searchResultList = new ArrayList<>();
for (Object collection : searchRepositoryService.findAllCollections(configJson).getJsonArray(SOLR_CLOUD_COLLECTION).getList()) {
searchResultList.add(new JsonObject(doSearchPerCollection(collection.toString(), requestMessage.toString())));
}
return aggregateMultiCollectionSearchResults(searchResultList);
}
public String doSearchPerCollection(String collection, String message) {
org.springframework.web.reactive.function.client.WebClient client =
org.springframework.web.reactive.function.client.WebClient.create();
return client.post()
.uri("http://127.0.0.1:8983/solr/" + collection + "/query")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(message.toString()))
.retrieve()
.bodyToMono(String.class)
.block();
}
private List<JsonObject> aggregateMultiCollectionSearchResults(List<JsonObject> searchList){
//TODO: Search result aggregation
return searchList;
}
My use case is the second verticle should make multiple vertx web client calls and should combine the responses.
If an API call falls, I want to log the error and still continue processing and merging responses from other calls.
Please, any help on how my code above could be adaptable to handle the use case?
I am looking at vertx CompositeFuture, but no headway or useful example seen yet!
What you are looking for can done with Future coordination with a little bit of additional handling:
CompositeFuture.join(future1, future2, future3).onComplete(ar -> {
if (ar.succeeded()) {
// All succeeded
} else {
// All completed and at least one failed
}
});
The join composition waits until all futures are completed, either with a success or a failure.
CompositeFuture.join
takes several futures arguments (up to 6) and returns a future that is succeeded when all the futures are succeeded, and failed when all the futures are completed and at least one of them is failed
Using join you will wait for all Futures to complete, the issue is that if one of them fails you will not be able to obtain response from others as CompositeFuture will be failed. To avoid this you should add Future<T> recover(Function<Throwable, Future<T>> mapper) on each of your Futures in which you should log the error and pass an empty response so that the future does not fail.
Here is short example:
Future<String> response1 = client.post(8887, "localhost", "work").expect(ResponsePredicate.SC_OK).send()
.map(HttpResponse::bodyAsString).recover(error -> {
System.out.println(error.getMessage());
return Future.succeededFuture();
});
Future<String> response2 = client.post(8887, "localhost", "error").expect(ResponsePredicate.SC_OK).send()
map(HttpResponse::bodyAsString).recover(error -> {
System.out.println(error.getMessage());
return Future.succeededFuture();
});
CompositeFuture.join(response2, response1)
.onSuccess(result -> {
result.list().forEach(x -> {
if(x != null) {
System.out.println(x);
}
});
})
.onFailure(error -> {
System.out.println("We should not fail");
});
Edit 1:
Limit for CompositeFuture.join(Future...) is 6 Futures, in the case you need more you can use: CompositeFuture.join(Arrays.asList(future1, future2, future3)); where you can pass unlimited number of futures.
my function is similar to:
#TestFactory
public Stream<DynamicTest> dynamicTest() throws Exception {
String geocodingAnasJsonTest = properties.getProperty("smart-road.simulator.json.geocoding-it.anas.testSuite.test");
String endpoint = properties.getProperty("smart-road.simulator.endpoint.anasGeocoding");
RequestSpecification request = RestAssured.given().header("Authorization", auth);
request.accept(ContentType.JSON);
request.contentType(ContentType.JSON);
JsonNode jsonObjectArray = JsonMappingUtil.getJsonFileFromPath(geocodingAnasJsonTest);
Stream<JsonNode> elementStream = StreamSupport.stream(Spliterators
.spliteratorUnknownSize(jsonObjectArray.elements(),
Spliterator.ORDERED), false);
return elementStream.map(jsonNode -> DynamicTest.dynamicTest(String.format("Test ID: %s", jsonNode.get("test_name")),
() -> {request.body(jsonNode.get("request").toString());
Response response = request.post(endpoint);
int statusCode = response.getStatusCode();
boolean res = false;
if (statusCode >= 200 && statusCode < 300) {
res = true;
}
try {
assertEquals(true, res, properties.getProperty("smart-road.response.smart-road.message.status.ok"));
logger.info(properties.getProperty("smart-road.response.smart-road.message.status.ok"));
String responseOK=jsonNode.get("response").toString();
assertEquals(responseOK, response.asString(), properties.getProperty("smart-road.response.smart-road.message.status.right-end"));
logger.info(properties.getProperty("smart-road.response.smart-road.message.status.right-end"));
} catch (AssertionFailedError er) {
logger.error(properties.getProperty("smart-road.response.smart-road.message.status.assertion-failed"));
fail("Test Fallito");
Assertions.assertTrue(true);
}
}
)//fine dynamicTest
);//fine map
}//fine metodo
I have 20 children test.
I run test in main:
SummaryGeneratingListener listener = new SummaryGeneratingListener();
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectMethod(Test.class,"dynamicTest"))
.build();
Launcher launcher = LauncherFactory.create();
launcher.registerTestExecutionListeners(listener);
launcher.execute(request);
Now with summary= listener.getSummary() i dont read all tests result but only count Failed or Successfull test.
How i read all result fail/success for all tests?
I will want a map like this:
TEST_ID - RESULTS
test0001 Success
test0002 Fail
test0003 Success
test0004 Success
test0005 Fail
How i get this? Is possible?
Thanks
Regards
One approach is to create your own implementation of org.junit.platform.launcher.TestExecutionListener and register it with the launcher. You may look at the source code of SummaryGeneratingListener as a first start. You could change executionFinished(..) to build up the map of test results. Here's a sketch:
class MySummaryListener implements TestExecutionListener {
private Map<String, TestExecutionResult.Status> summary = new HashMap<>();
#Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
summary.put(testIdentifier.getDisplayName(), testExecutionResult.getStatus());
}
}
There's probably more you want to do in the listener but it should give you an idea where to start.
The unit test keeps giving me =
Wanted but not invoked: However, there were exactly 3 interactions with this mock.
All I am trying to do is, testing the timeout for a method execution - if the method takes more time, then terminate it and publish count(to understand the timed out response rate) as metric.
#Test
public void testTimeoutFunction() throws Exception {
Response response = getResponseForTest();
when(processor
.process(any(Request.class)))
.thenAnswer((Answer<Response>) invocation -> {
Thread.sleep(100);
return response;
});
when(itemRequest.getRequestContext()).thenReturn(itemRequestContext);
testClass = spy(new TestClass(processor, executorService));
List<Item> output = testClass.getItemList(ID, itemRequest);
verify(testClass, times(1)).responseTimedOutCount();
assertTrue(output.isEmpty());
verify(testClass, timeout(EXECUTION_TIMEOUT)).buildResponse(itemRequest);
verify(testClass, times(1)).buildResponse(itemRequest);
}
This is method which I am testing for:
public class TestClass {
#VisibleForTesting
void responseTimedOutCount() {
//log metrics
}
private CompletableFuture<Response> getResponseAsync(final ScheduledExecutorService delayer,
final ItemRequest itemRequest) {
return timeoutWithTimeoutFunction(delayer, EXECUTION_TIMEOUT, TimeUnit.MILLISECONDS,
CompletableFuture.supplyAsync(() -> getResponseWithTimeoutFunction(itemRequest), executorService),
Response.emptyResponse(), () -> responseTimedOutCount());
}
private Response getResponseWithTimeoutFunction(final ItemRequest itemRequest) {
//do something and return response
}
public List<Item> getItemList(final String id, final ItemRequest itemRequest) throws Exception {
final ScheduledExecutorService delayer = Executors.newScheduledThreadPool(1);
Response response;
if(validateItemId(id){
try {
response = getResponseAsync(delayer, itemRequest).get();
} catch (final Throwable t) {
response = Response.emptyResponse();
} finally {
delayer.shutdown();
}
return transform(response, id).getItems();
} else {
return null;
}
}
}
Exception from Junit :
For this assert -
verify(testClass, times(1)).responseTimedOutCount();
Wanted but not invoked:
testClass.responseTimedOutCount();
However, there were exactly 3 interactions with this mock:
testClass.getItemList(ID, itemRequest);
testClass.validateItemId(ID);
testClass.getResponseWithTimeoutFunction(itemRequest);
I have following configuration for creation of two channels (by using the JmsChannelFactoryBean):
#Bean
public JmsChannelFactoryBean jmsChannel(ActiveMQConnectionFactory activeMQConnectionFactory) {
JmsChannelFactoryBean fb = new JmsChannelFactoryBean(true);
fb.setConnectionFactory(activeMQConnectionFactory);
fb.setDestinationName("something.queue");
fb.setErrorHandler(t -> log.error("something went wrong on jms channel", t));
return fb;
}
#Bean
public JmsChannelFactoryBean jmsChannelDLQ(ActiveMQConnectionFactory activeMQConnectionFactory) {
JmsChannelFactoryBean fb = new JmsChannelFactoryBean(true);
fb.setConnectionFactory(activeMQConnectionFactory);
fb.setDestinationName("something.queue.DLQ");
fb.setErrorHandler(t -> log.error("something went wrong on jms channel", t));
return fb;
}
The something.queue is configured to put the dead letter on something.queue.DLQ. Im using mostly Java DSL to configure the app, and if possible - would like to keep this.
Case is: the message is taken from jmsChannel put to sftp outbound gateway, if there is a problem on sending the file, the message is put back into the jmsChannel as not delivered. After some retries it is designed as poisonus, and put to something.queue.DLQ.
Is it possbile to have the info on error channel when that happens?
What is best practice to handle errors when using JMS backed message channels?
EDIT 2
The integration flow is defined as:
IntegrationFlows.from(filesToProcessChannel).handle(outboundGateway)
Where filesToProcessChannel is the JMS backed channel and outbound gateway is defined as:
#Bean
public SftpOutboundGateway outboundGateway(SftpRemoteFileTemplate sftpRemoteFileTemplate) {
SftpOutboundGateway gateway = new SftpOutboundGateway(sftpRemoteFileTemplate, AbstractRemoteFileOutboundGateway.Command.PUT.getCommand(), EXPRESSION_PAYLOAD);
ArrayList<Advice> adviceChain = new ArrayList<>();
adviceChain.add(errorHandlingAdvice());
gateway.setAdviceChain(adviceChain);
return gateway;
}
Im trying to grab exception using advice:
#Bean
public Advice errorHandlingAdvice() {
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
RetryTemplate retryTemplate = new RetryTemplate();
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(1);
retryTemplate.setRetryPolicy(retryPolicy);
advice.setRetryTemplate(retryTemplate);
advice.setRecoveryCallback(new ErrorMessageSendingRecoverer(filesToProcessErrorChannel));
return advice;
}
Is this the right way?
EDIT 3
There is certanly something wrong with SFTPOutboundGateway and advices (or with me :/):
I used the folowing advice from the spring integration reference:
#Bean
public Advice expressionAdvice() {
ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
advice.setSuccessChannelName("success.input");
advice.setOnSuccessExpressionString("payload + ' was successful'");
advice.setFailureChannelName("failure.input");
advice.setOnFailureExpressionString(
"payload + ' was bad, with reason: ' + #exception.cause.message");
advice.setTrapException(true);
return advice;
}
#Bean
public IntegrationFlow success() {
return f -> f.handle(System.out::println);
}
#Bean
public IntegrationFlow failure() {
return f -> f.handle(System.out::println);
}
And when I use :
return IntegrationFlows.from(filesToProcessChannel)
.handle((GenericHandler<File>) (payload, headers) -> {
if (payload.equals("x")) {
return null;
}
else {
throw new RuntimeException("some failure");
}
}, spec -> spec.advice(expressionAdvice()))
It gets called, and i get error message printed out (and that is expected), but when I try to use:
return IntegrationFlows.from(filesToProcessChannel)
.handle(outboundGateway, spec -> spec.advice(expressionAdvice()))
The advice is not called, and the error message is put back to JMS.
The app is using Spring Boot v2.0.0.RELEASE, Spring v5.0.4.RELEASE.
EDIT 4
I managed to resolve the advice issue using following configuration, still don't understand why the handler spec will not work:
#Bean
IntegrationFlow files(SftpOutboundGateway outboundGateway,
...
) {
return IntegrationFlows.from(filesToProcessChannel)
.handle(outboundGateway)
...
.log(LoggingHandler.Level.INFO)
.get();
}
#Bean
public SftpOutboundGateway outboundGateway(SftpRemoteFileTemplate sftpRemoteFileTemplate) {
SftpOutboundGateway gateway = new SftpOutboundGateway(sftpRemoteFileTemplate, AbstractRemoteFileOutboundGateway.Command.PUT.getCommand(), EXPRESSION_PAYLOAD);
ArrayList<Advice> adviceChain = new ArrayList<>();
adviceChain.add(expressionAdvice());
gateway.setAdviceChain(adviceChain);
return gateway;
}
#Bean
public ExpressionEvaluatingRequestHandlerAdvice expressionAdvice() {
ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
advice.setSuccessChannelName("success.input");
advice.setOnSuccessExpressionString("payload + ' was successful'");
advice.setFailureChannelName("failure.input");
advice.setOnFailureExpressionString(
"payload + ' was bad, with reason: ' + #exception.cause.message");
advice.setTrapException(true);
return advice;
}
#Bean
public IntegrationFlow success() {
return f -> f.handle(System.out::println);
}
#Bean
public IntegrationFlow failure() {
return f -> f.handle(System.out::println);
}
Since the movement to the DLQ is performed by the broker, the application has no mechanism to log the situation - it is not even aware that it happened.
You would have to catch the exceptions yourself and publish the message the the DLQ yourself, after some number of attempts (JMSXDeliveryCount header), instead of using the broker policy.
EDIT
Add an Advice to the .handle() step.
.handle(outboundGateway, e -> e.advice(myAdvice))
Where myAdvice implements MethodInterceptor.
In the invoke method, after a failure, you can check the delivery count header and, if it exceeds your threshold, publish the message to the DLQ (e.g. send it to another channel that has a JMS outbound adapter subscribed) and log the error; if the threshold has not been exceeded, simply return the result of the invocation.proceed() (or rethrow the exception).
That way, you control publishing to the DLQ rather than having the broker do it. You can also add more information, such as the exception, to headers.
EDIT2
You need something like this
public class MyAdvice implements MethodInterceptor {
#Autowired
private MessageChannel toJms;
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
return invocation.proceed();
}
catch Exception(e) {
Message<?> message = (Message<?>) invocation.getArguments()[0];
Integer redeliveries = messasge.getHeader("JMXRedeliveryCount", Integer.class);
if (redeliveries != null && redeliveries > 3) {
this.toJms.send(message); // maybe rebuild with additional headers about the error
}
else {
throw e;
}
}
}
}
(it should be close, but I haven't tested it). It assumes your broker populates that header.
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 ?
You can use WebAsyncTask to implement the timeout control and Thread management to stop the new async thread gracefully.
Implement a Callable to run the process
In this method (that runs in a diferent thread) store the current Thread in a Controller's local variable
Implement another Callable to handle timeout event
In this method retrieve the previously stored Thread and interrupt it calling the interrupt() method.
Also throw a TimeoutException to stop the controller process
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;
}