6

I'm trying to implement chunk response in webapp using PLay 2 with Akka. However, instead of load the response by chunk by chunk all the response is coming as once. Below is the code by which I'm creating chunk in the controller:

/**
 * 
 */    
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import akka.stream.javadsl.Source;
import akka.util.ByteString;
import org.pmw.tinylog.Logger;
import play.cache.CacheApi;
import play.cache.Cached;
import play.filters.csrf.AddCSRFToken;
import play.filters.csrf.CSRF;
import play.libs.Json;
import play.libs.concurrent.HttpExecutionContext;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Http.Cookie;
import play.mvc.Result;

import akka.NotUsed;
import akka.actor.Status;
import akka.stream.OverflowStrategy;
import akka.stream.javadsl.Source;
import akka.util.ByteString;

/**
 * @author Abhinabyte
 *
 */
@Singleton
@AddCSRFToken
public class GetHandler extends Controller {

    @Inject
    private CacheApi cache;

    @Inject
    private HttpExecutionContext httpExecutionContext;

    public CompletionStage<Result> index() {

return CompletableFuture.supplyAsync( () ->
            Source.<ByteString>actorRef(256, OverflowStrategy.dropNew())
                    .mapMaterializedValue(sourceActor -> {

                        CompletableFuture.runAsync(() -> {
                            sourceActor.tell(ByteString.fromString("1"), null);
                            sourceActor.tell(ByteString.fromString("2"), null);
                            sourceActor.tell(ByteString.fromString("3"), null);
                            try {
                                Thread.sleep(3000);//intentional delay
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            sourceActor.tell(ByteString.fromString("444444444444444444444444444444444444444444444444444444444444444444444444"), null);
                            sourceActor.tell(new Status.Success(NotUsed.getInstance()), null);
                        });

                        return sourceActor;
                    })
    ).thenApplyAsync( chunks -> ok().chunked(chunks).as("text/html"));  

    }

}


And below is the Akka thread pool configuration at application.conf :

akka {
  jvm-exit-on-fatal-error = on
  actor {
    default-dispatcher {
      fork-join-executor {
        parallelism-factor = 1.0
        parallelism-max = 64
        task-peeking-mode = LIFO
      }
    }
  }
}

play.server.netty {
  eventLoopThreads = 0
  maxInitialLineLength = 4096
  log.wire = false
  transport = "native"
}

As you can see before sending last to last chunk I'm intentionally delaying the response time. So logically, all chunked data before it should be delivered before it.
However, in my case whole bunch of data is getting loaded. I've tested in all browser(even have tried to CURL).
What I'm missing in here?

Abhinab Kanrar
  • 1,532
  • 2
  • 20
  • 46

3 Answers3

1

Blocking in mapMaterializedValue will do that because it runs in the Akka default-dispatcher thread, thus preventing message routing for the duration (see this answer for details). You want to dispatch your slow, blocking code asynchronously, with the actor reference for it to post messages to. Your example will do what you expect if you run it in a future:

public CompletionStage<Result> test() {
    return CompletableFuture.supplyAsync( () ->
            Source.<ByteString>actorRef(256, OverflowStrategy.dropNew())
                    .mapMaterializedValue(sourceActor -> {

                        CompletableFuture.runAsync(() -> {

                            for (int i = 0; i < 20; i++) {
                                sourceActor.tell(ByteString.fromString(String.valueOf(i) + "<br/>\n"), null);
                                try {
                                    Thread.sleep(500);//intentional delay
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            sourceActor.tell(new Status.Success(NotUsed.getInstance()), null);
                        });

                        return sourceActor;
                    })
    ).thenApplyAsync( chunks -> ok().chunked(chunks).as("text/html"));
}
Community
  • 1
  • 1
Mikesname
  • 8,781
  • 2
  • 44
  • 57
  • 1
    not working :( same thing is happening just like before – Abhinab Kanrar Aug 12 '16 at 05:09
  • Are you sure? I tested it myself... post a Gist of your whole controller. – Mikesname Aug 12 '16 at 06:24
  • 1
    updated code snippet with with whole controller code. Please have a look if I'm missing something – Abhinab Kanrar Aug 12 '16 at 06:50
  • I've expanded the explanation, but I'm stumped as to why it's still not working for you. Can you try this alternative, which uses an Actor for the blocking code: https://gist.github.com/mikesname/5b7124f51a8705eb7bdb0ff329255390 – Mikesname Aug 12 '16 at 09:21
  • Thanks for your effort. However, it's still not working resulting the same. I guess some silly mistake is happeing. Do I need to add any param in application.conf?Also, I've tried with all kinda browser but result is According to both play and scala guideline, the both of the codes you and I using should work. So what's the issue in here? – Abhinab Kanrar Aug 12 '16 at 09:46
  • Have you changed the default thread pool settings at all? And to confirm, even the actor version is outputting everything at once, yes? – Mikesname Aug 12 '16 at 09:51
  • Updated code snippet with thread pool config details. And, actor version is outputting everything at once. – Abhinab Kanrar Aug 12 '16 at 09:56
  • 1
    find out that the same code is working only in the Chrome 40+. Not working in any firefox or IE version. Do i need to do something for cross browser effectiveness? – Abhinab Kanrar Aug 17 '16 at 14:47
  • Hello @AbhinabKanrar , I can confirm, it works in Chrome, but not Safari. Have you found a solution yet? Thank you! – schube Jan 09 '18 at 21:00
  • @schube I found that most of the latest version of web-browser support chunk-encoding. Not sure about mobile based browser. There is nothing wrong from the application side since after monitoring the network packet, I found that there is not a single packet loss – Abhinab Kanrar Jan 10 '18 at 10:07
1

If you check the Source code, you can see that the first parameter is bufferSize

public static <T> Source<T,ActorRef> actorRef(int bufferSize,
                                               OverflowStrategy overflowStrategy)

all your elements that you generate in the stream probably have less then 256 bytes, hence only one http chunk is generated. Try to add more elements like in @Mikesname example.

Ion Cojocaru
  • 2,583
  • 15
  • 16
  • 1
    buffersize didn't solve the issue, but find out that the same code is working only in the Chrome 40+. Not working in any firefox or IE version. Do i need to do something for cross browser effectiveness? – Abhinab Kanrar Aug 17 '16 at 14:47
  • for testing you could use simple curl command with --no-buffer to show the data as it arrives, presumably some browsers have a local buffer that may output the data independent of how the data arrives. Still try to add way more data in the stream to properly see the chunking mechanism at work(http chunking makes sense only for large response entities) – Ion Cojocaru Aug 18 '16 at 06:59
  • 1
    as per my knowledge, firefox, chrome etc use 1024 buffer size. so if it's working in chrome it should also work in firefox and so on. Please correct me if I am wrong – Abhinab Kanrar Aug 18 '16 at 07:38
  • another thing to worry would the browser cache, this is why I pointed out testing via a third party tool like curl – Ion Cojocaru Aug 18 '16 at 08:07
0

It might me useful, if you need chunked response by using other approach.

public Result test() {
    try {
        // Finite list
        List<String> sourceList = Arrays.asList("kiki", "foo", "bar");
        Source<String, ?> source =  Source.from(sourceList);

    /*  Following DB call, which fetch a record at a time, and send it as chunked response.
        final Iterator<String> sourceIterator = Person.fetchAll();
        Source<String, ?> source =  Source.from(() -> sourceIterator);  */

        return ok().chunked(source.via(Flow.of(String.class).map(ByteString::fromString))).as(Http.MimeTypes.TEXT);

    } catch (Exception e) {
        return badRequest(e.getMessage());
    }
}
Sivakumar
  • 1,711
  • 14
  • 18
  • nope....same issue. Moreover, as per my knowledge, this seems not be a good design since you are copying all chunked data in the list first. Hence, there won't be any meaning of sending chunked data as the computational time already wasted :( – Abhinab Kanrar Aug 18 '16 at 07:33
  • [This](https://github.com/playframework/play-streaming-java) one might useful for you. – Sivakumar Aug 18 '16 at 08:34
  • Thanks for the link....the link you provided is an example of comet. I don't think chunk encoding would work as same in this case. The issue is in the browser side which needs some fixes – Abhinab Kanrar Aug 18 '16 at 08:52