11

I need to build a webservice with Jersey that downloads a big file from another service and returns to the client. I would like jersey to read some bytes into a buffer and write those bytes to client socket.

I would like it to use non blocking I/O so I dont keep a thread busy. (This could not be achieved)

    @GET
    @Path("mypath")
    public void getFile(final @Suspended AsyncResponse res) {
        Client client = ClientBuilder.newClient();
        WebTarget t = client.target("http://webserviceURL");
        t.request()
            .header("some header", "value for header")
                .async().get(new InvocationCallback<byte[]>(){

            public void completed(byte[] response) {
                res.resume(response);
            }

            public void failed(Throwable throwable) {
                res.resume(throwable.getMessage());
                throwable.printStackTrace();
                //reply with error
            }

        });
    }

So far I have this code and I believe Jersey would download the complete file and then write it to the client which is not what I want to do. any thoughts??

fredcrs
  • 3,558
  • 7
  • 33
  • 55

1 Answers1

6

The client side async request, isn't going to do much for your use case. It's more mean for "fire and forget" use cases. What you can do though is just get the InputStream from the client Response and mix with a server side StreamingResource to stream the results. The server will start sending the data as it is coming in from the other remote resource.

Below is an example. The "/file" endpoint is the dummy remote resource that serves up the file. The "/client" endpoint consumes it.

@Path("stream")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public class ClientStreamingResource {

    private static final String INFILE = "Some File";

    @GET
    @Path("file")
    public Response fileEndpoint() {
        final File file = new File(INFILE);
        final StreamingOutput output = new StreamingOutput() {
            @Override
            public void write(OutputStream out) {

                try (FileInputStream in = new FileInputStream(file)) {
                    byte[] buf = new byte[512];
                    int len;
                    while ((len = in.read(buf)) != -1) {
                        out.write(buf, 0, len);
                        out.flush();
                        System.out.println("---- wrote 512 bytes file ----");
                    }
                } catch (IOException ex) {
                    throw new InternalServerErrorException(ex);
                }
            }
        };
        return Response.ok(output)
                .header(HttpHeaders.CONTENT_LENGTH, file.length())
                .build();
    }
    
    @GET
    @Path("client")
    public void clientEndpoint(@Suspended final AsyncResponse asyncResponse) {
        final Client client = ClientBuilder.newClient();
        final WebTarget target = client.target("http://localhost:8080/stream/file");
        final Response clientResponse = target.request().get();

        final StreamingOutput output = new StreamingOutput() {
            @Override
            public void write(OutputStream out) {
                try (final InputStream entityStream = clientResponse.readEntity(InputStream.class)) {
                    byte[] buf = new byte[512];
                    int len;
                    while ((len = entityStream.read(buf)) != -1) {
                        out.write(buf, 0, len);
                        out.flush();
                        System.out.println("---- wrote 512 bytes client ----");
                    }
                } catch (IOException ex) {
                    throw new InternalServerErrorException(ex);
                }
            }
        };
        ResponseBuilder responseBuilder = Response.ok(output);
        if (clientResponse.getHeaderString("Content-Length") != null) {
            responseBuilder.header("Content-Length", clientResponse.getHeaderString("Content-Length"));
        }
        new Thread(() -> {
            asyncResponse.resume(responseBuilder.build());
        }).start();
    }
}

I used cURL to make the request, and jetty-maven-plugin to be able to run the example from the command line. When you do run it, and make the request, you should see the server logging

---- wrote 512 bytes file ----
---- wrote 512 bytes file ----
---- wrote 512 bytes client ----
---- wrote 512 bytes file ----
---- wrote 512 bytes client ----
---- wrote 512 bytes file ----
---- wrote 512 bytes client ----
---- wrote 512 bytes file ----
---- wrote 512 bytes client ----
...

while cURL client is keeping track of the results

enter image description here

The point to take away from this is that the "remote server" logging is happening the same time as the client resource is logging. This shows that the client doesn't wait to receive the entire file. It starts sending out bytes as soon as it starts receiving them.

Some things to note about the example:

  • I used a very small buffer size (512) because I was testing with a small (1Mb) file. I really didn't want to wait for a large file for testing. But I would imagine large files should work just the same. Of course you will want to increase the buffer size to something larger.

  • In order to use the smaller buffer size, you need to set the Jersey property ServerProperties.OUTBOUND_CONTENT_LENGTH_BUFFER to 0. The reason is that Jersey keeps in internal buffer of size 8192, which will cause my 512 byte chunks of data not to flush, until 8192 bytes were buffered. So I just disabled it.

  • When using AsyncResponse, you should use another thread, as I did. You may want to use executors instead of explicitly creating threads though. If you don't use another thread, then you are still holding up the thread from the container's thread pool.


UPDATE

Instead of managing your own threads/executor, you can annotate the client resource with @ManagedAsync, and let Jersey manage the threads

@ManagedAsync
@GET
@Path("client")
public void clientEndpoint(@Suspended final AsyncResponse asyncResponse) {
    ...
    asyncResponse.resume(responseBuilder.build());
}
    
Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • I dont get it........Your answer was great but it does not solve the problem. If I have 500 requests it will use 500 threads, you are using blocking IO still. Isnt it? – fredcrs Feb 22 '16 at 11:31
  • It is true that the download of the file works but when you are going to send the file it will block. So, a slow client would hold the thread for long time ( out.write(buf, 0, len) ) when you are uploading the file. – fredcrs Feb 22 '16 at 11:35
  • The point of Jersey's async APIs is not for Nonblocking IO in the traditional sense. What it's meant to do is not block the main request thread, by allowing you to handle the request in a separate thread. Even with your attempt with the client async, that is not Nonblocking IO either. What you you seem to be proposing is using Java's NIO. That is the only way I know how to do _real_ Nonblocking IO. And I am no NIO expert, so I don't even want to try to attempt implementing that. I don't even know how much of a performance gain you would get from it, if any vs. using threads. – Paul Samsotha Feb 22 '16 at 11:43
  • I will keep looking if there is a way to do with non blocking IO. But thanks anyway your answer was helpful – fredcrs Feb 22 '16 at 11:51
  • 1
    @fredcrs You might want to check out [ChunkedOutput](https://jersey.java.net/documentation/latest/async.html#chunked-output). I was going through the documentation, and if you look [here (scroll down to the note right before 15.4.2)](https://jersey.java.net/documentation/latest/sse.html#d0e11579) and it tells the difference between `StreamingOutput` and `ChunkedOutput`. I think you may get the non-blocking behavior you are looking for. Though I haven't done any testing with this – Paul Samsotha Mar 03 '16 at 16:36