16

I'm implementing long polling as per the Spring blog from some time ago.

Here my converted method with same response signature as before, but instead of responding immediately, it now uses long polling:

private Map<String, DeferredResult<ResponseEntity<?>>> requests = new ConcurrentHashMap<>();

@RequestMapping(value = "/{uuid}", method = RequestMethod.GET)
public DeferredResult<ResponseEntity<?>> poll(@PathVariable("uuid") final String uuid) {
    // Create & store a new instance
    ResponseEntity<?> pendingOnTimeout = ResponseEntity.accepted().build();
    DeferredResult<ResponseEntity<?>> deferredResult = new DeferredResult<>(TWENTYFIVE_SECONDS, pendingOnTimeout);
    requests.put(uuid, deferredResult);

    // Clean up poll requests when done
    deferredResult.onCompletion(() -> {
        requests.remove(deferredResult);
    });

    // Set result if already available
    Task task = taskHolder.retrieve(uuid);
    if (task == null)
        deferredResult.setResult(ResponseEntity.status(HttpStatus.GONE).build());
    else
        // Done (or canceled): Redirect to retrieve file contents
        if (task.getFutureFile().isDone())
            deferredResult.setResult(ResponseEntity.created(RetrieveController.uri(uuid)).build());

    // Return result
    return deferredResult;
}

In particular I'd like to return the pendingOnTimeout response when the request takes too long (which I returned immediately before), to prevent proxies from cutting off the request.

Now I think I've gotten this working as is, but I'd like to write a unittest that confirms this. However all my attempts at using MockMvc (via webAppContextSetup) fail to provide me with a means of asserting that I get an accepted header. When I for instance try the following:

@Test
public void pollPending() throws Exception {
    MvcResult result = mockMvc.perform(get("/poll/{uuid}", uuidPending)).andReturn();
    mockMvc.perform(asyncDispatch(result))
            .andExpect(status().isAccepted());
}

I get the following stacktrace:

java.lang.IllegalStateException: Async result for handler [public org.springframework.web.context.request.async.DeferredResult> nl.bioprodict.blast.api.PollController.poll(java.lang.String)] was not set during the specified timeToWait=25000 at org.springframework.util.Assert.state(Assert.java:392) at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:143) at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:120) at org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch(MockMvcRequestBuilders.java:235) at nl.bioprodict.blast.docs.PollControllerDocumentation.pollPending(PollControllerDocumentation.java:53) ...

The Spring framework tests related to this that I could find all use mocking it seems: https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTimeoutTests.java

How can I test the correct handling of the DeferredResult timeoutResult?

Tim
  • 19,793
  • 8
  • 70
  • 95
  • To be clear: It seems to work fine in integration tests, but I'd also want to test this in `spring-restdocs-mockmvc`. – Tim Dec 17 '15 at 20:08
  • I have just run into this exact same issue. Did you ever find a solution that allows testing of the timeouts on DeferredResults? – John Haager May 04 '16 at 19:04
  • @John nope, not yet, although I've stopped looking for now.. Let me know if you find anything! – Tim May 05 '16 at 20:48
  • @Tim I need to test the same case, were you able to find the solution? – rd22 Oct 10 '16 at 05:52
  • 1
    @Tim, I just received the same error, and the cause was that the reference inside the `DeferredResult` was `null`. Hope it helps. – riccardo.cardin Mar 14 '18 at 14:54

2 Answers2

13

In my case, after going through spring source code and setting the timeout (10000 millisecond) and getting async result solved it for me, as;

 mvcResult.getRequest().getAsyncContext().setTimeout(10000);
 mvcResult.getAsyncResult();

My whole test code was;

MvcResult mvcResult = this.mockMvc.perform(
                                post("<SOME_RELATIVE_URL>")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(<JSON_DATA>))
                        ***.andExpect(request().asyncStarted())***
                            .andReturn();

***mvcResult.getRequest().getAsyncContext().setTimeout(10000);***
***mvcResult.getAsyncResult();***

this.mockMvc
    .perform(asyncDispatch(mvcResult))
    .andDo(print())
    .andExpect(status().isOk());

Hope it helps..

myuce
  • 1,321
  • 1
  • 19
  • 29
  • 1
    Thanks! Upvoted as I feel it might help someone out, but in my case it does not yet work either with `@SpringBootTest()` nor `@WebMvcTest(PollController.class)`. Both keep throwing up: `Deferred result was not set during the specified timeToWait=25000`.. Thanks though! – Tim Jul 11 '17 at 13:36
6

I ran across this problem using Spring 4.3, and managed to find a way to trigger the timeout callback from within the unit test. After getting the MvcResult, and before calling asyncDispatch(), you can insert code such as the following:

MockAsyncContext ctx = (MockAsyncContext) mvcResult.getRequest().getAsyncContext();
for (AsyncListener listener : ctx.getListeners()) {
    listener.onTimeout(null);
}

One of the async listeners for the request will invoke the DeferredResult's timeout callback.

So your unit test would look like this:

@Test
public void pollPending() throws Exception {
    MvcResult result = mockMvc.perform(get("/poll/{uuid}", uuidPending)).andReturn();
    MockAsyncContext ctx = (MockAsyncContext) result.getRequest().getAsyncContext();
    for (AsyncListener listener : ctx.getListeners()) {
        listener.onTimeout(null);
    }
    mockMvc.perform(asyncDispatch(result))
            .andExpect(status().isAccepted());
}
Kenster
  • 23,465
  • 21
  • 80
  • 106