Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jersey: immediate Response after asynchronous request

I try to understand the way asynchronous responses work with Jersey. I read chapter 10 of the Jersey documentation (https://jersey.java.net/documentation/latest/async.html) but it doesn't help with my problem. Also research here on stackoverflow didn't result in satisfying answers (that I can understand).

What I'm trying to do is similar to the one question in this post (Use http status 202 for asynchronous operations). I want to upload a large file to the server using a HTML form document. After the request is send to the server the web service should immediately response with status 202 and a URI where the file can be found after the request has finished.

After reading the post abive it seems possible but sadly no hints how to implement such a behavior where given.

I wrote a small web service to test the functionality:

    @Path("/test/async/")
    public class TestAsyncResponse {

        @GET
        @Path("get")
        public Response asyncGet(@Suspended final AsyncResponse response) {

            new Thread(new Runnable() {

                @Override
                public void run() {

                    DateFormat df = new SimpleDateFormat("dd/MM/yy HH:mm:ss");

                    System.out.println("#### thread started: " 
                                       + df.format(new Date()) + " ####");
                    String result = veryExpensiveOperation();
                    System.out.println("#### thread finished: " 
                                        + df.format(new Date()) + " ####");

                    response.resume(result);
                }

                private String veryExpensiveOperation() {

                    try {

                        Thread.sleep(10000);
                    } 
                    catch (InterruptedException e) {

                        e.printStackTrace();
                    }

                    return "Woke up!";
                }
            }).start();

            return Response.status(202).entity("Request accepted. " + 
                                        "Long running operation started")
                                        .build();
        }
    }

The service works but as a response I get the "Woke Up!" message after the 10 second wait rather than the 202 response which seems logical because the AsyncResponse is the one that handles the response (as I understand it).

After reading the documentation I got the impression that this is suppose to happen because all Jersey does with the asynchronous server response is to outsource the thread from the response thread pool to another one to free processing time for more responses to the service.

So my two questions would be: Is my understanding correct and can I use the asynchronous server response to get the desired behavior?

I tried to start a new thread without the AsyncResponse and I get a NullPointerExceptionbecause Jersey already closed the response and thus closed the InputStream that contains the file data. Is this the expected behavior? This post (https://stackoverflow.com/a/17559684/1416602) seems to indicate that it might work.

Any response is greatly appreciated.

Greetings

like image 681
Scyla Avatar asked Jul 31 '14 15:07

Scyla


2 Answers

Your question is mixing two topics.

From HTTP perspective, 202 is technically a completed request. And the result of the request is 202, server telling you it will do it on the side. You will have to make another HTTP request to get updated status.

From the perspective of your application, async means that you will execute the request in a separate thread (or other async way). But also, this means that you will not return a result, not even 202, until the other "veryExpensiveOperation" finishes. The whole point in jumping through this hoop is to free up the calling thread. Your web server has a limited number, e.g. 20, and if each of your requests took a very long time, all 20 would be hanging. Using @Suspended you transfer execution from the web server thread to some other means, (another thread in your case). This is really only the first step. The idea behind async servers is that even the veryExpensiveOperation is implemented in some async way so that waiting for a DB or a file does not occupy a whole thread.

like image 80
mikijov Avatar answered Oct 13 '22 01:10

mikijov


I have been through the same pain recently. Jersey keeps claiming it supports Asynchronous REST calls, but I think it's being disingenuous. And in fact, once I started to work out the correct way of doing this, Jersey actually got in the way.

private static ExecutorService executorService = Executors.newFixedThreadPool( Integer.valueOf( numberOfThreads ) );

@POST
@Path("async")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response async( @FormDataParam("file") InputStream inputStream,
        @FormDataParam("file") FormDataContentDisposition des ) throws Throwable {

    String uniqueID = UUID.randomUUID().toString();
    executorService.execute( new Runnable() {

        @Override
        public void run() {
            try {
               // do long performing action
            } catch (Exception ex) {
            }
        }            

   } );
   return Response.accepted().location( getResultsURI( uniqueID ) ).build();
}

@GET
@Path("results/{uniqueID}")
@Produces("application/zip")
public Response results( @PathParam(value = "uniqueID ") String uniqueID ) {

   // Check status of job
   // If not finished...
   if (notFinished) {
   return Response.status( 202 ).location( getResultsURI( uniqueID ) )
                .entity( status ).build();
   }

   return Response.ok( FileUtils.readFileToByteArray( zip.toFile() ) ).type( "application/zip" )
            .header( "Content-Disposition", "attachment; filename=\"filename.zip\"" ).build();
  }

protected URI getResultsURI( String uniqueID ) throws URISyntaxException {
    return new URI( Constants.WS_VERSION + "/results/" + uniqueID );
}

The biggest pain was that when you set Response.location(), even if you set it to "./results" or "/results", Jersey expands it to the full URL. Which would be fine, except that it ignores any class-level @Path:

@Path(Constants.WS_VERSION)
public class MyEndpoint {

So instead of fighting it, I used the above code to at least make it correct. Ideally I'd like Jersey to leave the "Location" header alone.

Anyway - the above code is what I used (excluding the business logic bits ;) )

like image 23
cs94njw Avatar answered Oct 13 '22 00:10

cs94njw