Buscar este blog

domingo, 7 de febrero de 2016

Spring Controller and Future Tasks

Suppose you have a web application in which the user can ask for reports. The application collects some data and then generate a PDF (or CSV, or whatever) and returns it to him.

Depending on what kind of report and what kind of filter data, the time the app requires to build the report may vary. So, for the same report, the user can get the file immediately, or may need to wait some seconds to get it.

Now, you dont want to force the user to wait for generated the report when it takes much time. If the application detects that the report is lasting much, instead the file, it returns a message telling him that his file will be sent by other means (for example, by sending an email later).

This behaviour can be achieved with Asynchronous tasks, and Futures.

We have a Controller which receives the user request and then calls a service. The service constructs the report, returns it to the controller, and finally the controller returns it to the user. This is the basic flow of much Spring MVC applications.

In this example I have a Service called InvoiceReportService which returns a InvoiceReport.
public class InvoiceReport {
 private byte[] content;

 public byte[] getContent() {
  return content;
 }

 public void setContent(byte[] content) {
  this.content = content;
 }
}

The service will be executed asynchronously so, instead returns a InvoiceReport object it will return a Future<InvoiceReport>:
public interface InvoiceReportService {

 Future<InvoiceReport> generateReport();

}

This is the service class. It just reads a PDF file from the classpath and returns it. I added a random delay to simulate some heavy tasks running. Note that the service method is annotated with @Async.
@Service
public class InvoiceReportServiceImpl implements InvoiceReportService {
 
 @Override
 @Async
 public Future<InvoiceReport> generateReport() {
  Resource resource = new ClassPathResource("spring-boot-reference.pdf");    
  byte[] bytes = null;
  try {
   bytes = IOUtils.toByteArray(resource.getInputStream());
  } catch (IOException e) {
   //Nothing     
  }
  
  simulateDelay();
  
  InvoiceReport report = new InvoiceReport();
  report.setContent(bytes);
  return new AsyncResult<InvoiceReport>(report);
 }

 
 private void simulateDelay() {
  int secondsSleep = new Random().nextInt(2);
  try {
   System.out.println("Going to sleep " + secondsSleep + " seconds");
   TimeUnit.SECONDS.sleep(secondsSleep);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  System.out.println("Awake!!");
 }
}

Working with futures you can specify a maximum time you want to wait for the result. If the result is not ready in this time, a TimeOutException is raised, but the underlying task is not interrupted.
So, the controller could be something like this:
@Controller
public class InvoiceController {
 
 @Autowired
 private InvoiceReportService invoiceReportSerive;
 
 @RequestMapping(value="/report", method=RequestMethod.POST)
 public HttpEntity<byte[]> getReport(HttpServletResponse response) throws IOException {
  Future<InvoiceReport> reportFutre = invoiceReportSerive.generateReport();
  
  InvoiceReport invoiceReport = null;
  try {
   invoiceReport = reportFutre.get(1, TimeUnit.SECONDS);
   System.out.println("In time");  
   return responseNow(invoiceReport);
  }
  catch(TimeoutException e) {
   System.out.println("Too late");  
   response.sendRedirect("/laterPlease");
   return null;
  }
  catch(Exception e) {
   //Handle exception
   return null;
  }
 }

 
 private HttpEntity<byte[]> responseNow(InvoiceReport invoiceReport) { 
     HttpHeaders header = new HttpHeaders();
     header.setContentType(new MediaType("application", "pdf"));
     header.set("Content-Disposition", "attachment; filename= \"report.pdf\"");
     header.setContentLength(invoiceReport.getContent().length);

     return new HttpEntity<byte[]>(invoiceReport.getContent(), header); 
 }
}

The controller waits, as much, as one second for the report. If the service does not respond in that time, then shows the user a info page. Otherwise, the controller returns the file.

But this has one problem. How does the user get the report when it is not completed in the specified timeout? We need other task which wait for report as long as necessary, because the service will be running even when the controller returns the info page.

To solve this I created a second service which is the one who will wait until the first one ends.
@Service
public class WaitUntilFinishService<T> {
 
 @Async
 public void waitUntilFinish (Future<T> future, AfterWaitComand<T> afterWaitCommand){
  try {
   T result = future.get();
   afterWaitCommand.docommand(result);
  } catch (InterruptedException | ExecutionException e) {
   System.out.println("The task was interrupted");
   e.printStackTrace();
  }
 }
}
This service receives the Future result and just waits for it. It also receives a call back service, so when the task is completed, it calls this call back service in order to complete the primary action.

This callback service implements a common interface, so you can wire any custom implementation you need. In this case, for example, the callback could retrieve the file and send it to the user by mail.
public interface AfterWaitComand<T> {
 void docommand(T result);
}

@Service
public class SendMailAfterWaitCommand implements AfterWaitComand<InvoiceReport> {

 @Override
 public void docommand(InvoiceReport result) {
  System.out.println("Sending invoice result by mail");
  System.out.println(result);  
 }

}

Whit thease improvements, the controller will be as follows:
@Controller
public class InvoiceController {
 
 @Autowired
 private InvoiceReportService invoiceReportSerive;
 
 @Autowired
 private WaitUntilFinishService<InvoiceReport> waitUntilFinishService;
 
 @Autowired
 private AfterWaitComand<InvoiceReport> sendMailAfterWaitCommand;
 
 
 
 @RequestMapping(value="/report", method=RequestMethod.POST)
 public HttpEntity<byte[]> getReport(HttpServletResponse response) throws IOException {
  Future<InvoiceReport> reportFutre = invoiceReportSerive.generateReport();
  
  InvoiceReport invoiceReport = null;
  try {
   invoiceReport = reportFutre.get(1, TimeUnit.SECONDS);
   System.out.println("In time");  
   return responseNow(invoiceReport);
  }
  catch(TimeoutException e) {
   System.out.println("Too late");
   waitUntilFinishService.waitUntilFinish(reportFutre, sendMailAfterWaitCommand);   
   response.sendRedirect("/laterPlease");
   return null;
  }
  catch(Exception e) {
   //Handle exception
   return null;
  }
 }

 
 private HttpEntity<byte[]> responseNow(InvoiceReport invoiceReport) { 
     HttpHeaders header = new HttpHeaders();
     header.setContentType(new MediaType("application", "pdf"));
     header.set("Content-Disposition", "attachment; filename= \"report.pdf\"");
     header.setContentLength(invoiceReport.getContent().length);

     return new HttpEntity<byte[]>(invoiceReport.getContent(), header); 
 }
}


You can download the source from my github: https://github.com/evazquezma/JEE6/tree/master/async

No hay comentarios:

Publicar un comentario