8

I have a comet(long polling) Controller call, which takes in some ids and puts then into a blocking queue if no calculation for that id is running, for a Consumer to take from the queue and perform computations on these ids. I am using Springs DeferredResult for asynch support.

I maintain a Map of DeferredResult and the corresponding ids that were received in a request. When the calculations for a id are complete in the consumer thread I check for this id in the Map and set the associated DeferredResults setResult which send the response back to the client.

In the Controller method I have an onCompletion callback of DeferredResult which removes this DeferredResult object from the Map.

The client then removes this id from its request and sends the remaining ids. As as example say the client send initially ids "1,2,3" they all got inserted into BlockingQueue and say the calculation for id "2" was finished earlier, then its DeferredResults setResult will be set, which will return the response to the client. And through the callback this DeferredResult will be removed from the Map. And the client in the next request will send ids "1, 3".

Now all works fine, but when I started writing test cases for this the onCompletion callback is never called. I even tried it in the springs official examples it does not seem to be called even there. Is there a way to call it, or something is incorrect in my implementation.

This is my Controller method:

@RequestMapping(value = "views/getLongPollingGraphData", method = RequestMethod.GET, headers = "Accept=application/json")
@ResponseBody
public DeferredResult<WebServiceResponse> getLongGraphData(
        HttpServletRequest request,
        @RequestParam(value = "ids") String ids) 
{
    //create a default response in case, when no result has been calculated till timeout
    WebServiceResponse awrDefault = new WebServiceResponse();

    //set time out this DeferredResult, after 5 seconds 
    final DeferredResult<WebServiceResponse> deferredResult = new DeferredResult<WebServiceResponse>(5000L, awrDefault);

    //logic to set in blocking queue
    //mMapOfDeferredResultAndViews.put(deferredResult, listOfViews.keySet());

    deferredResult.onCompletion(new Runnable() {
        @Override
        public void run() 
        {
            mMapOfDeferredResultAndViews.remove(deferredResult);
        }
    });

    return deferredResult;
}

Below is a part of the test case:

@Before
public void setup() 
{
    this.mockMvc = webAppContextSetup(this.wac).build();
}

@Test
public void testLongPollGraphData()
{
    try
    {         
        String ids = "22,23,25";
        List<Integer> idList = convertStringToList(ids);

        while(idList.size() > 0)
        {
            MvcResult mvcResult = this.mockMvc.perform(get("/views/getLongPollingGraphData")
                .contentType(MediaType.APPLICATION_JSON)
                .param("ids", ids)          
                .andReturn();

            this.mockMvc.perform(asyncDispatch(mvcResult));
            WebServiceResponse result = (WebServiceResponse)mvcResult.getAsyncResult();

            if(result != null)
            {
                EJSChartsData chartsData = (EJSChartsData)result.getResponse();
                if(chartsData != null && chartsData.getViewId() != -1)
                {
                    int viewId = chartsData.getViewId();
                    idList.remove((Integer)viewId);
                    ids = idList.toString().replace("[", "").replace("]", "");
                }
            }
        }
    }
    catch(Exception e)
    {
        fail(e.toString());
    }
}

The Controller method is called and I receive the response as expected, but as the onCompletion callback is never called my logic of calling the method again by removing the id takes a hit as the Map holds on the past DeferredResult.

Update

The test class annotations are:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/test-context.xml" })
@WebAppConfiguration
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)

I have another observation, the call to getLongPollingGraphData is not completed, i.e. it does not wait for the specified timeout of 5 seconds and returns immediately causing the result to be null. I read about it and the recommendation is to add .andExpect(request().asyncResult("Expected output")) to mockMvc object.

But this is the whole point of my test case, I want the result to be calculated and returned so that I can resend a request modifying my ids variable based on the response received.

Alykoff Gali
  • 1,121
  • 17
  • 32
rd22
  • 1,032
  • 1
  • 18
  • 34
  • Show your Test Class Annotations at top – shazin Oct 10 '16 at 05:19
  • @shazin updated in the question, with some additional observations. – rd22 Oct 10 '16 at 05:49
  • Do you use tomcat for your web application? – eg04lt3r Oct 13 '16 at 21:04
  • @eg04lt3r Yes, I do. – rd22 Oct 14 '16 at 05:51
  • @eg04lt3r But what does it has to do with the test case? – rd22 Oct 14 '16 at 06:55
  • Am I understanding correctly when I say you specifically want to test the behaviour when a timeout occurs? – Leon Oct 14 '16 at 12:45
  • I tried to write test for my custom controller which return deferredresult with some timeout and default value. I set nothing as result and I was expecting that I receive default value after timeout released. But I had no luck just error that async result still empty after timeout. Also I have not seen anywhere in spring tests this test case. – eg04lt3r Oct 14 '16 at 19:55
  • @eg04lt3r Yes this is what I have observed. However the async result could be waited for using `asyncDispatch()` and expecting the result while `mockMvc.perform()` atleast this is what the test cases said in Springs. Surprisingly there is no mention of timeout and `onComplete`. – rd22 Oct 15 '16 at 04:02
  • @Leon No, I want my `onComplete` to be called when the response is returned from the controller. – rd22 Oct 15 '16 at 04:04
  • @rd22 What I mean is you want to check that onComplete is called when you receive a timeout. If that is the case you are not alone. http://stackoverflow.com/questions/34343231/how-to-test-deferredresult-timeoutresult – Leon Oct 15 '16 at 06:43
  • @Leon Yes, I could find this question, I even posted a comment asking if he was able to achieve it. Nevertheless calling `onComplete` on timeout is one requirement, other is to call it when a normal response is send. – rd22 Oct 15 '16 at 12:50
  • @rd22, the main problem is in DefaultMvcResult::getAsyncResult. It uses defined timeout and checking if result set during timeout, if not raise exception. It is not using timeoutResult, and I think it's not proper implementation of this method. Because I tested my controller with curl, and I received default result and onComplete was called. – eg04lt3r Oct 15 '16 at 15:28
  • @rd22, Also obvious that timeoutResult should be set as result only after timeout released. Current code (DefaultMvcResult) does not support this behavior. – eg04lt3r Oct 15 '16 at 15:34

1 Answers1

0

What version of Spring are you using? I've opened SPR-13615 for a similar case and it got fixed in 4.2.3 (affects 4.2.2).

If you look at the comments, you can workaround it without updating by using

    mvcResult.getRequest().getAsyncContext().complete();
kewne
  • 2,208
  • 15
  • 11