Weird thread behavior between a filter and the controller


(Arnaud Villevieille) #1

Hello there,

Play 2.6.20

I am running into a strange behavior from Play when using a filter similar to that one:

public class WhateverFilter extends EssentialFilter {

	private final SpecialExecutor ec;

	@Inject
    public WhateverFilter(SpecialExecutor ec) {
    	this.ec = ec;
    }

    @Override
    public EssentialAction apply(EssentialAction next) {
        return EssentialAction.of(
                request -> {
                    final String path = request.path();

                        Accumulator<ByteString, Result> accumulator = next.apply(request);
                        Flow<ByteString, ByteString, NotUsed> flow =
                                Flow.<ByteString>create()
                                        .map(
                                                in -> {
                                                    //Whatever
                                                    return in;
                                                });
                        return accumulator
                                .through(flow)
                                .recover(
                                        throwable -> {
                                            //Whatever
											return ok();
                                        },
										ec.executorWithThreadLocalCopyToTheNextThread())
                                .map(
                                        result -> {
                                            //Whatever
                                            return result;
                                        },
										ec.executorWithThreadLocalCopyToTheNextThread());
                    });
    }
}

For some reasons, I need to control the next’s thread call, that is why I explicitly pass the Executor to all those async operations. The reason why is that I need to copy something (a thread local variable) from the first thread to the next one. I am not the only one to do that, the purpose being to carry around some kind of request scoped variable.
This works fine most of the time from that filter or between any actor operation in the Play framework.

One exception though:

With any kind of HTTP verb (get, post…) if i set a Content-Type (json for example) and send “{}” in the body, the thread of the controller will be different from the thread of the filter. Without the body it will always be the same.

I guess my question here is: is that expected behavior? And if yes, why with and without a body the behavior differs? And is there a way for me to highjack whoever decides to start a new thread between the controller and the filter to be able to pass my thread-local variable?


(Arnaud Villevieille) #2

By using the debugger, i noticed that when there is a body, we call the class BodyParser. In there, inside the method “apply”, we run something like that:

@Override
        public Accumulator<ByteString, F.Either<Result, A>> apply(Http.RequestHeader request) {
            Flow<ByteString, ByteString, Future<MaxSizeStatus>> takeUpToFlow = Flow.fromGraph(play.api.mvc.BodyParsers$.MODULE$.takeUpTo(maxLength));
            Sink<ByteString, CompletionStage<F.Either<Result, A>>> result = apply1(request).toSink();

            return Accumulator.fromSink(takeUpToFlow.toMat(result, (statusFuture, resultFuture) ->
               FutureConverters.toJava(statusFuture).thenCompose(status -> {
                  if (status instanceof MaxSizeNotExceeded$) {
                      return resultFuture;
                  } else {
                      return errorHandler.onClientError(request, Status$.MODULE$.REQUEST_ENTITY_TOO_LARGE(), "Request entity too large")
                              .thenApply(F.Either::<Result, A>Left);
                  }
               })
            ));
        }

When that FutureConverters is executed we are not even running inside an akka thread but a common commonPool thread from java.

I imagine that because of that context switch we loose the initial thread. Though i am not sure.


(Marcos Pereira) #3

Well,

There are no guarantees that the same thread will be used, but I think you are right we should not use the common pool here. I’m not this will help, but try to use HttpExecutionContext since it manages HTTP thread local state (Http.Context) and store the state inside the Http.Context.args.

Best.


(Arnaud Villevieille) #4

Yes, we used something like that to bypass our problem and it works. THank you