1

I have a Java Spring Service with a RestController that calls an async method:

@RestController
public class SomeController {

  @Autowired
  //this is the service that contains the async-method
  OtherService otherService;

  @GetMapping
  public void someFunctionWithinTheMainRequestThread() {
    otherService.asyncMethod(RequestContextHolder.getRequestAttributes());
  }
}

That async method needs to use the RequestContextAttributes because it is building Links with linkTo(...). The problem is that no matter how I pass the RequestAttributes to the method, I always get the error

java.lang.IllegalStateException: Cannot ask for request attribute - request is not active anymore!

This is the annotation on the async method:

public class OtherService {

  @Async
  @Transactional(readOnly = true)
  public void asyncMethod(RequestAttributes context) {
    RequestContextHolder.setRequestAttributes(context);
    //doing a lot of stuff that takes a while
    linkTo(methodOn(...)) //-> here the error occurs
  }

What I tried:

  • Passing RequestAttributes manually as a Parameter (as seen in the code-snippets above)
  • Using the context-aware-pool executor described in this answer: How to enable request scope in async task executor - which basically seems to do the same as if I pass the context as a variable only that is is configured globally
  • Updating the servlet config and setting ThreadContextInheritable to true
  • Assigning the RequestAttributes to a final variable to try to get a copy of the original object which is marked as inactive by the main thread

No matter what I do, the request always seems to finish before my async method and I apparently never have a deep copy of the Attributes so they always get marked as inactive by the main thread before the async method is finished and then I can't use them anymore -> at least that is my understanding of the error.

I just want to be able to get the requestAttributes needed for the linkTo method in my async method even after the main thread finished the request, can someone point me in the right direction?

Juliette
  • 966
  • 16
  • 35
  • You didn't include any code. The usual approaches would be to explicitly copy the necessary data as input to the service call and/or to join the result. – chrylis -cautiouslyoptimistic- Jan 10 '21 at 09:06
  • @chrylis-cautiouslyoptimistic- I can't manually pass the needed parameters because linkTo internally uses some LinkBuilder which then uses RequestContextHolder.getAttributes -> so I need to get the stuff into the current RequestContext and can't just pass them as a parameter – Juliette Jan 10 '21 at 09:14
  • Oh, so this is Spring HATEOAS with all of its attendant headaches. Can you pass an `EntityLinks`? – chrylis -cautiouslyoptimistic- Jan 10 '21 at 10:02
  • You need to add the entire stack trace of the exception. – tgdavies Jan 10 '21 at 10:35

1 Answers1

1

I found a solution that does work and removes the error. Since I don't think this is really clean I am hoping for more answers but in case it helps someone:

First I added this class. It creates a custom and very simple RequestAttributes-Implementation that enables us to keep the Attributes active for longer than they normally would be:

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

public class AsyncRequestScopeAttr extends ServletRequestAttributes {
    private Map<String, Object> requestAttributeMap = new HashMap<>();

    public AsyncRequestScopeAttr(HttpServletRequest request) {
        super(request);
    }

    @Override
    public void requestCompleted() {
        //keep the request active, normally here this.requestActive would be set to false -> we do that in the completeRequest()-method which is manually called after the async method is done
    }

    /**
     * This method should be called after your async method is finished. Normally it is called when the
     * request completes but since our async method can run longer we call it manually afterwards
     */
    public void completeRequest()  {
        super.requestCompleted();
    }

    @Override
    public Object getAttribute(String name, int scope) {
        if(scope== RequestAttributes.SCOPE_REQUEST) {
            return this.requestAttributeMap.get(name);
        }
        return null;
    }
    @Override
    public void setAttribute(String name, Object value, int scope) {
        if(scope== RequestAttributes.SCOPE_REQUEST){
            this.requestAttributeMap.put(name, value);
        }
    }
    @Override
    public void removeAttribute(String name, int scope) {
        if(scope== RequestAttributes.SCOPE_REQUEST) {
            this.requestAttributeMap.remove(name);
        }
    }
    @Override
    public String[] getAttributeNames(int scope) {
        if(scope== RequestAttributes.SCOPE_REQUEST) {
            return this.requestAttributeMap.keySet().toArray(new String[0]);
        }
        return  new String[0];
    }
    @Override
    public void registerDestructionCallback(String name, Runnable callback, int scope) {
        // Not Supported
    }
    @Override
    public Object resolveReference(String key) {
        // Not supported
        return null;
    }
    @Override
    public String getSessionId() {
        return null;
    }
    @Override
    public Object getSessionMutex() {
        return null;
    }

    @Override
    protected void updateAccessedSessionAttributes() {

    }
}

Then in the RestController before the async method is called:

@Autowired
//this is the service that contains the async-method
OtherService otherService;

public void someFunctionWithinTheMainRequestThread(){

   otherService.asyncMethod(getIndependentRequestAttributesForAsync());

}

private RequestAttributes getIndependentRequestAttributesForAsync(){
    RequestAttributes requestAttributes = new AsyncRequestScopeAttr(((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest());
    for (String attributeName : RequestContextHolder.getRequestAttributes().getAttributeNames(RequestAttributes.SCOPE_REQUEST)) {
        RequestContextHolder.getRequestAttributes().setAttribute(attributeName, RequestContextHolder.getRequestAttributes().getAttribute(attributeName, RequestAttributes.SCOPE_REQUEST), RequestAttributes.SCOPE_REQUEST);
    }
    return requestAttributes;
}

And then in the async function:

public class OtherService {


  @Async
  @Transactional(readOnly=true)
  public void asyncMethod(RequestAttributes context) {

    //set the RequestAttributes for this thread
    RequestContextHolder.setRequestAttributes(context);

    // do your thing .... linkTo() etc.

    //cleanup      
    ((AsyncRequestScopeAttr)context).completeRequest();
    RequestContextHolder.resetRequestAttributes();

  }

}
Juliette
  • 966
  • 16
  • 35