11

I have a long running (4-10 second) MVC action that runs a report from an AJAX call. While it's running users can change the parameters and run something else, so I cancel the AJAX request before making another one.

So, for instance (example in jQuery but the problem happens regardless):

// If we have an active request and it's not complete
if(dataRequest && dataRequest.readyState != 'complete') 
{
    dataRequest.abort();
}

dataRequest = $.ajax(...);

Client side this appears to work fine, but the cancelled request is still running on the server. For instance if the report takes 10 seconds, and I cancel one and start the other then the second request takes 20 seconds.

I think this is due to the session level locking:

[If] two concurrent requests are made for the same session (by using the same SessionID value), the first request gets exclusive access to the session information. The second request executes only after the first request is finished.

So the second request can't access the session until the first one finishes. Using asynchronous MVC actions doesn't seem to get around this as the action still needs to be able to make changes to the session.

Is it possible to stop one action and start the next without using AsyncController or [SessionState(SessionStateBehavior.ReadOnly)]?

If it isn't are both required?

Keith
  • 150,284
  • 78
  • 298
  • 434

3 Answers3

4

[SessionState(SessionStateBehavior.ReadOnly)] or completely disabling session is enough to allow concurrent access to controller actions from the same session. So the second request (from the same session) doesn't need to wait for the first to complete before being processed. As far as canceling the first action on the server is concerned, well, that will be harder. You will have to associate each task an unique id and when a new task is started return this task id to the client. Then when you cancel the AJAX request on the client by calling .abort() you could fire another AJAX request to some other controller action and pass it the unique task id. This controller action itself will set some common flag to indicate the first action to stop.

The TPL has built-in cancellation support that you might take a look to simplify that in order to avoid a shared data structure between the 2 that will synchronize them.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • I'm already using the TPL to do a lot of the work that's taking the time - I'm executing about 25s worth of long running consecutive code in about 4-10s parallel. However using it to handle the abort does seem like reinventing the wheel; why does the new request need to tell the old (still running) one to abort when it's already been cancelled? – Keith Jan 19 '12 at 08:33
  • 1
    @Keith, it's not the new request that's gonna tell the old to abort. You have to send a new special AJAX request to tell the server to abort the processing of a particular task. When you do `dataRequest.abort();` you are only aborting the AJAX request but if you want to abort the server side processing you need to do much more work than that. – Darin Dimitrov Jan 19 '12 at 08:36
  • Hmm, so IIS doesn't know that the request has been cancelled at the transport level? I would have expected the page thread to be aborted at the IIS/ASP level when the waiting connection is terminated at the transport level. – Keith Jan 19 '12 at 08:40
  • IIS does - all I need to do is check `Response.IsClientConnected` within the long running page. When that property becomes false I cancel what I'm doing. – Keith Jan 31 '12 at 16:08
4

As it turns out the complete answer required two things:

Firstly (thanks Darin for confirming it) the session essentially locks the pages to be executed one after the other. Further investigation has pointed to this being a more general problem with ASP.Net sessions - they just can't handle optimistic concurrency.

Secondly the cancelled request needs to check whether the client is still connected. There are situations where you might want to continue (like a fire and forget ASP action) but in this case if the client's no longer waiting for the report there's no point us continuing to process it.

This is available as a property of the response: this.Response.IsClientConnected

So to effectively cancel my server side action when jQuery requested it I had to write my own session handler and add regular checks against this.Response.IsClientConnected.

Community
  • 1
  • 1
Keith
  • 150,284
  • 78
  • 298
  • 434
  • 1
    Be careful relying on `IsClientConnected` as it can apparently be a serious performance issue to check it, see http://stackoverflow.com/questions/9094735/when-is-response-isclientconnected-slow – Keith Feb 29 '12 at 14:09
  • Hi Keith! I have a similar problem but couldn't solve it even with your answer, (http://stackoverflow.com/questions/29235850/how-to-abort-long-running-ajax-request-in-net-mvc-4) Is there any way to help me out? Or could you post a sample code of yours to trigger `this.Response.IsClientConnected` – E-A Mar 26 '15 at 09:04
1

.abort() is only client-side cancellation. If you want to monitor it on the server side, one of the good ways to do it is to poll for IsClientConnected property, which will tell you what is the state of ajax comunication.

Dave
  • 349
  • 1
  • 15
  • 1
    See the accepted answer and the original question. This answer doesn't add anything not already stated. Also be extremely careful using `IsClientConnected` as it can apparently be a serious performance issue to check it, see http://stackoverflow.com/questions/9094735/when-is-response-isclientconnected-slow (apparently it can cost up to ½s to check it, but no-one has been able to confirm) – Keith Feb 29 '12 at 14:18
  • You are right, I've read your post and I have to revise my solutiuon. – Dave Feb 29 '12 at 15:09