75

A few days back I asked this question:

Why does $.getJSON() block the browser?

I fire six jQuery async ajax requests at the same controller action pretty much all at once. Each request takes 10 seconds to return.

Through debugging and logging requests to the action method I notice that the requests are serialised and never run in parallel. i.e. I see a timeline in my log4net logs like this:

2010-12-13 13:25:06,633 [11164] INFO   - Got:1156
2010-12-13 13:25:16,634 [11164] INFO   - Returning:1156
2010-12-13 13:25:16,770 [7124] INFO   - Got:1426
2010-12-13 13:25:26,772 [7124] INFO   - Returning:1426
2010-12-13 13:25:26,925 [11164] INFO   - Got:1912
2010-12-13 13:25:36,926 [11164] INFO   - Returning:1912
2010-12-13 13:25:37,096 [9812] INFO   - Got:1913
2010-12-13 13:25:47,098 [9812] INFO   - Returning:1913
2010-12-13 13:25:47,283 [7124] INFO   - Got:2002
2010-12-13 13:25:57,285 [7124] INFO   - Returning:2002
2010-12-13 13:25:57,424 [11164] INFO   - Got:1308
2010-12-13 13:26:07,425 [11164] INFO   - Returning:1308

Looking at the network timeline in FireFox I see this:

alt text

Both the log sample above and the Firefox network timeline are for the same set of requests.

Are requests to the same action from the same page serialised? I'm aware of serialised access to the Session object in the same session, but no session data is being touched.

I stripped the client side code down to a single request (the longest running one) but this still blocks the browser, i.e. only when the ajax request completes does the browser respond to any link clicking.

What I also observe here (in Chrome's developer tools) is that upon clicking on a link when a long running ajax request is executing it reports a Failed to load resource error immediately which suggests that the browser has killed (or is attempting to kill and waiting?) the ajax request:

alt text

However the browser still takes an age to redirect to the new page.

Are ajax requests really asynchronous or is this sleight of hand because javascript is actually single threaded?

Are my requests just taking too long for this to work?

The problem occurs in Firefox and IE as well.

I also changed the script to use $.ajax directly and explicitly set async: true.

I'm running this on IIS7.5, both the Windows 2008R2 and Windows 7 flavours do the same thing.

Debug and release builds also behave the same.

Community
  • 1
  • 1
Kev
  • 118,037
  • 53
  • 300
  • 385
  • Did you test this in firefox/IE. Maybe its a chrome issue? – A G Dec 13 '10 at 11:43
  • @aseem - it's the same in F/Fox and IE – Kev Dec 13 '10 at 11:45
  • Maybe your script changes the async setting somewhere else. – ZippyV Dec 13 '10 at 11:54
  • Can you check in FF/Firebug when the multiple requests are send(Net Panel in firebug). Those timelogs does not seem right, jquery ajax requests are async for sure and must run in parallel. – A G Dec 13 '10 at 11:56
  • @aseem - I've updated the question to show a revised log and the matching firefox timeline. – Kev Dec 13 '10 at 12:15
  • @anton - IIS7.5. @aseem - can't set a bounty for two days. – Kev Dec 13 '10 at 12:34
  • If you would take a closer look: 1913 is serviced before 1912. 1308 is serviced before 2002. If the browser was sending synchronous requests this would not be the case. – A G Dec 13 '10 at 12:36
  • @aseem - the firefox timeline is out of order I'm guessing because all the requests are placed simultaneously and there's nothing between them time wise. But of you look at the order of completion it does match the logging in the action. and the logging in the action shows that the requests are indeed serialised. – Kev Dec 13 '10 at 12:46
  • Well the firebug timeline is build as requests are send. So its always sequential. You can verify this by expanding a node and checking the date in header. You will see all send at same time. – A G Dec 13 '10 at 13:04
  • 1
    @aseem - there are no "sent" times in the request headers. What I'm saying is that because they all get requested at once firefox (or google) can't sort by any sensible order. The order that is there is due to the time taken for each underlying XHR to respond and acknowledge that "yes indeed I have sent this request" so there may be a few milliseconds of difference between each one due to differences in making a TCP/IP connection. Hence the out of order timeline. However I know for a fact these calls are serialised (the logging in the controller action tells me this).....cont... – Kev Dec 13 '10 at 13:17
  • @aseem ...also if I just have a 10seconds Thread.Sleep() (instead of the actual code that queries our back end services) again I see all the calls being serialised both in the browser and server side. Each call takes 10s one after the other. There is no parallel execution. I'd at least expect 2 calls to complete at the same time. – Kev Dec 13 '10 at 13:19
  • @aseem - I've updated the question with the results from a request where I introduce a 0.1s delay between each request just to allow each XHR request enough time to return an ack that the request has been sent. So now you can see exactly what is happening – Kev Dec 13 '10 at 13:34
  • I can see now.. crazy stuff.. – A G Dec 13 '10 at 13:37
  • @aseem - it truly is a bit mad. – Kev Dec 13 '10 at 14:30
  • @Kev - this is probably just silly, but are your JS calls in the $(document).ready() or somewhere else on the page? I suppose that if they are somewhere else they might block the browser from continuing with the rendering but $(document).ready() would/should not. – Hector Correa Dec 13 '10 at 18:08
  • @Hector - they're in `$(document).ready()`. – Kev Dec 13 '10 at 19:21
  • @hector @aseem - take a look at my answer, something to watch out for. – Kev Dec 14 '10 at 00:26
  • @Kev - thanks for sharing the answer. I would have never guessed that and definitively something to watch for. – Hector Correa Dec 14 '10 at 13:45

2 Answers2

91

The answer was staring me in the face.

ASP.NET Session State Overview:

Access to ASP.NET session state is exclusive per session, which means that if two different users make concurrent requests, access to each separate session is granted concurrently. However, 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.

Annoyingly I'd skimmed paragraph this a couple of weeks ago not really taking in the full impact of the bold sentences. I had read that simply as "access to session state is serialised" and not "all requests, no matter whether you touch session state or not, are serialised" if the requests came from the same session.

Fortunately there is a work around in ASP.NET MVC3 and its possible to create session-less controllers. Scott Guthrie talks about these here:

Announcing ASP.NET MVC 3 (Release Candidate 2)

I installed MVC3 RC2 and upgraded the project. Decorating the controller in question with [SessionState(SessionStateBehavior.Disabled)] solves the problem.

And of course typically I just found this in Stack Overflow a few minutes ago:

Asynchronous Controller is blocking requests in ASP.NET MVC through jQuery

Community
  • 1
  • 1
Kev
  • 118,037
  • 53
  • 300
  • 385
  • Thanks for the help with a tough bug! Is it just me or is this glaringly stupid default behavior? – theycallmemorty Sep 24 '12 at 18:38
  • 6
    Note that using `[SessionState(SessionStateBehavior.Disabled)]` may open security breach into your application, for instance access to private data in multiple simultaneous request, with this option you have no way to know is an user is logged, nor if a session is active, etc... If you need to access to the session you can use `[SessionState(SessionStateBehavior.ReadOnly)]`, so you have still access to your session, though you can't edit it, but still may help in many cases. – Gregoire D. Nov 13 '12 at 09:35
  • 1
    @GregoireD. - Thanks for the info about `SessionStateBehavior.ReadOnly`. I've a feeling I looked at that and that it still causes serialised access to your `Session`. The app uses Forms Authentication plus a cookie/db lookup combo to lock down access to that controller. The data returned from the controller itself isn't confidential, but we did take measures to prevent people fiddling out of idle curiosity. – Kev Jan 11 '13 at 14:11
  • 1
    SessionStateBehaviour.ReadOnly seems to be working in my case, and still allows access to the Session. – Miika L. Sep 17 '13 at 12:21
9

I was trying to reproduce this but was unable. Here's my test:

private static readonly Random _random = new Random();

public ActionResult Ajax()
{
    var startTime = DateTime.Now;
    Thread.Sleep(_random.Next(5000, 10000));
    return Json(new { 
        startTime = startTime.ToString("HH:mm:ss fff"),
        endTime = DateTime.Now.ToString("HH:mm:ss fff") 
    }, JsonRequestBehavior.AllowGet);
}

And the call:

<script type="text/javascript" src="/scripts/jquery-1.4.1.js"></script>
<script type="text/javascript">
    $(function () {
        for (var i = 0; i < 6; i++) {
            $.getJSON('/home/ajax', function (result) {
                $('#result').append($('<div/>').html(
                    result.startTime + ' | ' + result.endTime
                ));
            });
        }
    });
</script>

<div id="result"></div>

And the results:

13:37:00 603 | 13:37:05 969
13:37:00 603 | 13:37:06 640
13:37:00 571 | 13:37:07 591
13:37:00 603 | 13:37:08 730
13:37:00 603 | 13:37:10 025
13:37:00 603 | 13:37:10 166

And the FireBug console:

alt text

As you can see the AJAX action is hit in parallel.


UPDATE:

It seems that in my initial tests the requests are indeed queued in FireFox 3.6.12 and Chrome 8.0.552.215 when using $.getJSON(). It works fine in IE8. My tests were performed with a ASP.NET MVC 2 project, VS2010, Cassini web server, Windows 7 x64 bit.

Now if I replace $.getJSON() with $.get() it works fine under all browsers. That leads me to believe that there is something with this $.getJSON() which might cause the requests to queue. Maybe someone more familiar with the internals of the jQuery framework would be able to shed more light on this matter.


UPDATE 2:

Try setting cache: false:

$.ajax({
    url: '/home/ajax', 
    cache: false,
    success: function (result) {
        $('#result').append($('<div/>').html(
            result.startTime + ' | ' + result.endTime
        ));
    }
});
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • @Kev, yes it's a bit strange. Could you try replacing `$.getJSON()` by `$.get()`? I tested this with Cassini. I don't have IIS installed and cannot verify. – Darin Dimitrov Dec 13 '10 at 12:52
  • @darin - I tried `$.getJSON()` and went straight to `$.ajax()` and the results are the same. – Kev Dec 13 '10 at 13:04
  • Running this sample in IE8 works fine for me. However in FF and Chrome the requests are not sent in parallel. I´m using cassini and VS2010. – uvita Dec 13 '10 at 15:45
  • @uvita, does replacing `$.getJSON()` with `$.get()` make any difference? For me it makes a difference. When using `$.getJSON` the requests seem to be queued. – Darin Dimitrov Dec 13 '10 at 15:57
  • @darin, no it didn´t. Exactly the same results in every browser. – uvita Dec 13 '10 at 16:11
  • @uvita, please see my second update. Try setting `cache: false` and use `$.ajax`. – Darin Dimitrov Dec 13 '10 at 16:17
  • @uvita, yeap same behavior here. – Darin Dimitrov Dec 13 '10 at 16:26
  • I can´t figure out why is the cache option making the difference. Setting the cache option to false globally in ajaxsetup and using getJSON worked fine (as getJSON is a shorthand for $.ajax) – uvita Dec 13 '10 at 16:44
  • @darin @uvita - I set the `cache: false` option, don't sure if you noticed, but that appends a unique query string value to the request to prevent caching. However my problem still remains albeit it's a bit intermittent now. Thanks for the updates though. – Kev Dec 13 '10 at 20:42
  • @darin @uvita - think I'm going to fire up adplus and get a dump of the worker process and examine in windbg+sos to see if this is a server side issue. this is very odd. – Kev Dec 13 '10 at 22:26
  • @darin @uvita - take a look at the answer I just posted. Problem solved, I'm going to take a leap of faith with MVC3RC2 and put this into production (it's not a hugely critical application and it won't be long until RTM). Thanks guys for the major effort spent analysing this problem. – Kev Dec 14 '10 at 00:28
  • Very old comment thread, but it is possible to have false success if the app pool has Maximum Worker Processes set to greater than 1, because the app can then run the additional requests on the extra threads. I don't know how this applies to Cassini, but Cassini results should be taken with a grain of salt. – friggle Apr 09 '15 at 16:38