5

I am trying to make progress bar. On the server side I created a controller, which should get the progress value and store the value in session.

On the client side I created two ajax requests. One of them starts a function, which I want to be monitoring, and the other one just checks the progress stage. I mean, that's how I thought it would work. But it only outputs something when it's done.

It just waits for few seconds and then alerts "Done!" and that's all.

What's the problem? Maybe I should to create new thread in the controller to monitoring the progress stage?

Here's the client side code:

function generate() {

    setTimeout(checkProgress, 400);
        $.ajax({
            type: "POST",
            url: "/PPTReport/Generate",
            async: true,
            success: function (text) {
                 console.log(text);

            },
            error:function() {
                 console.log("error! error!");
            }
        });

  }

function checkProgress() {

    var id = "reportProgress";

    $.ajax({
        type: "POST",
        url: "/PPTReport/GetProgress",
        async: true,
        data: "id="+id,
        success: function (data) {
            if (data.done) {
                 console.log('Done!');
            } else {
                 console.log('Progress at ' + data.percent + '%');
                console.log(data.percent);
                setTimeout(checkProgress, 100 );
            }

        },
        error: function() {
            console.log('ajax error');
        }
    });
}

Here's the server side code

public class ProgressInfo
    {
        public int Percent { get; set; }
        public bool Done { get; set; }
    }

 public JsonResult GetProgress(string id)
        {
            ProgressInfo progress;
            if (string.IsNullOrEmpty(id)
                || (progress = Session[id] as ProgressInfo) == null)
            {
                return Json(new
                {
                    success = false
                });
            }
            if (progress.Done)
            {
                Session.Remove(id);
            }
            return Json(new
                {
                    success = true,
                    done = progress.Done,
                    percent = progress.Percent
                }
            );
        }


public JsonResult Generate(){
                    //init stuff 
                    //there are somtheing like that
                    ProgressInfo progress = new ProgressInfo();
                    progress.Percent = 0;
                    progress.Done = false;
                    if (tabs > 0)
                    {
                        FirstPage();
                        progress.Percent++;
                        Session["reportProgress"] = progress;
                        if (tabs > 1)
                        {
                            SecondPage();
                            progress.Percent++;
                            Session["reportProgress"] = progress;
                            if (tabs > 2)
                            {
                                ThirdPage();
                                progress.Percent++;
                                Session["reportProgress"] = progress;
                                if (tabs > 3)
                                {
                                   LastPage();
                                }
                             }
                         }
                     }
                   //what we've gonna return stuff etc 
         }

UPD: well, I finally kinda make it - I made test function (almost like in example) Here's the code:

js:

 function doCalculation() {
            $.post('/PPTReport/DoCalculation/sas');
            setTimeout(pollProgress, 1000);
        }

        function pollProgress() {
            var progressID = "sas";
            $.post('/PPTReport/GetProgress/' + progressID, function (response) {
                if (!response.success) {
                    alert('Cannot find progress');
                    return;
                }
                if (response.done) {
                    alert('Done!');
                } else {
                    alert('Progress at ' + response.percent + '%');
                    setTimeout(pollProgress, 1000);
                }
            }, 'json');

}

server-side:

public void DoCalculation(string id)
        {
            ProgressInfo progress = new ProgressInfo();
            if (!string.IsNullOrEmpty(id))
            {
                Session[id] = progress;
            }

            //periodicly update progress
            int i = 0;
            while(i == 0 && progress.Percent != 7600000340)
            {
                progress.Percent++;
                Thread.Sleep(1000);
            }
        }

        public JsonResult GetProgress(string id)
        {
            ProgressInfo progress;
            if (string.IsNullOrEmpty(id)
                || (progress = Session[id] as ProgressInfo) == null)
            {
                return Json(new
                {
                    success = false
                });
            }
            if (progress.Done)
            {
                Session.Remove(id);
            }
            return Json(new
            {
                success = true,
                done = progress.Done,
                percent = progress.Percent
            });
        }

but I wait more than a minute until the action will execute for the first time! Look enter image description here

Why does it happen? It just calling simple function, why does it wait so long?

kingsfoil
  • 3,795
  • 7
  • 32
  • 56
DanilGholtsman
  • 2,354
  • 4
  • 38
  • 69
  • 2
    I agree with skarist. You are seeing a classic session race condition here. What we did in a similar situation is call the client from the server using `SigalR`. Whenever we update progress in the session, we also send an update to the client. Works very well and `SignalR` is super simple to get going. – Elad Lachmi Jul 24 '14 at 06:58
  • @EladLachmi well, the teamleader said thats forbiidden – DanilGholtsman Jul 24 '14 at 08:53
  • idk why actually, but thats how it is – DanilGholtsman Jul 24 '14 at 08:54
  • You could use Cache instead of Session to store the progress. You can even use the session Id as part of the Cache key. You cannot have one request writing to the session and one reading concurrently, since the one writing will have an exclusive lock. – Elad Lachmi Jul 24 '14 at 09:24
  • @EladLachmi Oh well, seems legit. I seek for some examples – DanilGholtsman Jul 24 '14 at 09:30

2 Answers2

7

This is an instance of an old problem, see here:

http://msdn.microsoft.com/en-us/library/ms178581.aspx

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. (The second session can also get access if the exclusive lock on the information is freed because the first request exceeds the lock time-out.) If the EnableSessionState value in the @ Page directive is set to ReadOnly, a request for the read-only session information does not result in an exclusive lock on the session data. However, read-only requests for session data might still have to wait for a lock set by a read-write request for session data to clear.

I think you can find numerous posts on SO that discuss this. Here is one:

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

and here is one workaround:

Disable Session state per-request in ASP.Net MVC

Ok adding some (untested!) example code. This is just to indicate how to solve this:

public class myCache {

    private static Dictionary<string, ProgressInfo> _pinfos = new Dictionary<string, ProgressInfo>();
    private static readonly object _lockObject = new object();

    public static void Add(string key, ProgressInfo value)
    {
        lock (_lockObject)
        {
            if (!_pinfos.ContainsKey(key)) {
                _pinfos.Add(key, value);
            }
            else {
               _pinfos[key] = value;
            }

        }
    }

    public static ProgressInfo Get(string key) {
        if (_pinfos.ContainsKey(key)) {
          return _pinfos[key];
        }
        else {
          return null;
        }
    }

    public static void Remove(string key) {         
        lock (_lockObject)
        {
          if (_pinfos.ContainsKey(key)) {
             _pinfos.Remove(key);
          }
        }
    }
}




[SessionState(SessionStateBehavior.ReadOnly)]
public class TestController : AsyncController

    public void DoCalculation(string id)
        {
            ProgressInfo progress = new ProgressInfo();
            if (!string.IsNullOrEmpty(id)) {
                myCache.Add(id,progress);
            }

            //periodicly update progress
            int i = 0;
            while(i == 0 && progress.Percent != 7600000340)
            {
                progress.Percent++;
                Thread.Sleep(1000);
            }
        }

        public JsonResult GetProgress(string id)
        {
            ProgressInfo progress;
            if (string.IsNullOrEmpty(id)
                || (progress = myCache.Get(id)) == null)
            {
                return Json(new
                {
                    success = false
                });
            }
            if (progress.Done)
            {
                myCache.Remove(id);
            }
            return Json(new
            {
                success = true,
                done = progress.Done,
                percent = progress.Percent
            });
        }
}
Community
  • 1
  • 1
skarist
  • 1,030
  • 7
  • 9
  • oh, well, thanks, I gonna try to do something. not fully understood though – DanilGholtsman Jul 24 '14 at 03:21
  • and btw does it acting like a async method? because of there no any new threads or stuff like ._. and why does the test code working but after almost two minutes – DanilGholtsman Jul 24 '14 at 03:36
  • Or could ajax really make server side things sort of async? – DanilGholtsman Jul 24 '14 at 03:44
  • The reason your second code works is that the first request exceeds the session lock timeout. – skarist Jul 24 '14 at 07:48
  • and what do you think about cache using? – DanilGholtsman Jul 24 '14 at 10:02
  • 1
    Yes, caching would be one way to achieve this (e.g. System.Web.Caching). Generally speaking you need to have a variable on the application level to solve this, i.e. some variable that is not stored in the controller session. Then you can mark your controller with [SessionState(SessionStateBehavior.ReadOnly)]. Well, why even use a session store if you don't need it? In that case you can simply disable session state at the application level and gain faster performance. – skarist Jul 24 '14 at 10:36
  • why should I do this `[SessionState(SessionStateBehavior.ReadOnly)]`? I mean, I got there some other variables and session callings. Is it so necessary? And +1 btw – DanilGholtsman Jul 24 '14 at 10:44
  • Ok if you need the session state, then don't do this! I just wanted to point out some possibilities. But if DoCalculation and GetProgress are in a controller that has SessionStateBehavoir.ReadOnly then you won't experience session locking. But then of course you can't write anything into the session either. – skarist Jul 24 '14 at 10:53
  • Oh, I kinda confused. Could you please write some example code, like how to cache data inside inner `if` blocks like in my main code? – DanilGholtsman Jul 24 '14 at 10:56
  • oh and I think `[SessionState(SessionStateBehavior.ReadOnly)]` would work fine in my case, actually – DanilGholtsman Jul 24 '14 at 11:06
  • Ok added some example code. Check it out. It is just to demonstrate the idea. It uses a custom simple minded cache. Keep in mind that I haven't worked with .NET for 5 years, and don't even have it installed on my machine (using ubuntu now...) – skarist Jul 24 '14 at 11:50
  • Do you mean `_lockObject`? – DanilGholtsman Jul 24 '14 at 12:09
  • 1
    Perhaps better to use [SessionState(SessionStateBehavior.Disabled)], since you are not using the session state at all in this solution – skarist Jul 24 '14 at 12:14
  • 1
    @skarist - Why not just use ASP.NET Cache? And if using a Dictionary, you can use `ConcurrentDictionary`, which takes care of locking internally. I just think it`s less error-prone than doing the locking manually. – Elad Lachmi Jul 24 '14 at 18:49
  • Just want to add, that when you need to dig a small hole, a bulldozer is not always the best choice. Better to use a shovel. So, I would argue that when the requirements aren't to complex (e.g. Danil's problem as described), then it makes more sense to roll your own "cache" solution rather than to use System.Web.Caching. The sysem cache comes with all sorts of features and overhead (e.g. timeout/clearing configs etc.) that aren't really required. But sure, using ConcurrentDictionary seems a better choice to implement myCache. – skarist Jul 24 '14 at 23:43
2

Well Skarist explained WHY it's happening and what I was running into as well. I made an (incorrect) assumption that changing the Session State to read only would prevent me from writing to the Session, but it doesn't.

What I did to solve my problem is set the EnableSessionState to "ReadOnly" on the page I was assigning the session values in as well. This basically makes the Session not thread safe and allowed me to pull the values when I polled the server. I'm using WebForms for this at the moment, but it should be similar in MVC using [SessionState(SessionStateBehavior.Disabled)].

Matti Price
  • 3,351
  • 15
  • 28
  • yeah it might also work in MVC. But as you point out you sacrifice thread safety. As Danil describes his problem it shouldn't matter much for him, but as soon we have more complicated request patterns from clients we soon run into problems. So, I wouldn't recommend it. Anyway, Danil asked for the reason for his problem, and my answer provided the explanation. – skarist Jul 25 '14 at 00:51