views:

518

answers:

3

I have a Generic HTTP Handler (*.ashx) in my ASP.Net application which performs some basic but time consuming computations printing progress statements to the output as it goes to keep the user informed. Performing these computations involves reading some data files which are locked by the handler while using them, so it's important that two calls to the Handler don't start processing at once.

To achieve this I added a variable into the Cache that indicates the computation is in progress, this prevents the main application sending a user to this Handler if another user is already there. In the Handler itself it checks if the Cache variable is set and should send the user back to the main application if the Cache value is set. But when I test this by accessing the Handler twice one access executes fine and the second sits there and does nothing until the first finishes when it runs. Setting IsReusable to true makes no difference.

Anyone got any ideas why this happens?

Code below:

public class UpdateStats : IHttpHandler
{
    private HttpContext _context;

    public const String UpdateInProgressCacheKey = "FAHLeagueWebUpdateInProgress";

    public void ProcessRequest(HttpContext context)
    {
        //Use a Cache variable to ensure we don't call multiple updates
        Object inprogress = context.Cache[UpdateInProgressCacheKey];
        if (inprogress != null)
        {
            //Already updating
            context.Response.Redirect("Default.aspx");
        }
        else
        {
            //Set the Cache variable so we know an Update is happening
            context.Cache.Insert(UpdateInProgressCacheKey, true, null, DateTime.Now.AddMinutes(10), Cache.NoSlidingExpiration);
        }

        context.Response.Clear();
        context.Response.ContentType = "text/html";
        this._context = context;

        context.Response.Write("<pre>Please wait while we Update our Statistics, you will be automatically redirected when this finishes...\n\n");

        //Get the Stats
        Statistics stats = new Statistics(context.Server);

        //Subscribe to Update Progress Events
        stats.UpdateProgress += this.HandleUpdateProgress;

        //Update
        String force = context.Request.QueryString["force"];
        stats.UpdateStats((force != null));

        //Remove the Cache variable
        context.Cache.Remove(UpdateInProgressCacheKey);

        context.Response.Write("</pre>");
        context.Response.Write("<meta http-equiv=\"refresh\" content=\"0;URL=Default.aspx\" />");
        context.Response.Write("<p>If you are not automatically redirected please click <a href=\"Default.aspx\">here</a></p>");
    }

    private void HandleUpdateProgress(String message)
    {
        this._context.Response.Write(message + "\n");
        this._context.Response.Flush();
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

Edit

Added the code from the Master Page of the main application:

public partial class FAH : System.Web.UI.MasterPage
{
    private Statistics _stats;

    protected void Page_Init(object sender, EventArgs e)
    {
        this._stats = new Statistics(this.Server);
        if (this._stats.StatsUpdateNeeded)
        {
            //If the Cache variable is set we're already updating
            Object inprogress = Cache[UpdateStats.UpdateInProgressCacheKey];
            if (inprogress != null) this.Response.Redirect("UpdateStats.ashx");
        }
    }
    //etc...
}
A: 

If both threads read the cache value inprogress before one of them sets it, then inprogress will be null in both cases.

I think you may need some locking here as you might have some concurrency issues with the above code.

Matthew Pelser
I guess that could happen but that would only result in the main application redirecting the requests back to the Handler anyway since if the update doesn't happen then the main application will still want an update
RobV
I think having both threads with a null cache means you could hit context.Cache.Insert twice, so you wont block the second request. But this is not the problem you are describing. There may also be a similar timing issue with the line "if (inprogress != null) this.Response.Redirect("UpdateStats.ashx");" as this will have to make a round trip back to the browser to redirect to the handler.How are you making your 2 test requests? is it in the same browser?
Matthew Pelser
A: 

Are you sure your webserver is multithreaded and can execute the page simultaneously? Could you add print statements at the start of the ProcessRequest and at the end and see if you get in there simultaneously?

Ron
When debugging using VS2008 the second request won't even drop into the ProcessRequest method until the first finishes. I'm running IIS7 on Windows Server 2008 on my Quad core development box so I'm mulithreaded heavily
RobV
It's not because your CPU or OS is multithreaded that your webserver is. Since the second request doesn't come into the ProcessRequest method before the first finishes it's obvious that the server handles the requests sequentially. This is a limitation of the server application, maybe it can be configured, I don't have experience with that. You could try to see how it behaves with simultaneous requests on two different pages, maybe the sequential handling is only happening per page. This also means your inprogress mechanism is "useless" at the moment.
Ron
+1  A: 

Stumbled across the answer myself, it's nothing to do with the web server or the application but merely to do with browser behaviours. It seems that if you open several tabs and navigate to the same URL in a browser like Firefox or Chrome the browser makes the requests sequentially i.e. it waits for one to finish before making the next. Opening two browsers and making the two requests results in the expected behaviour

http://stackoverflow.com/questions/307936/iis-asp-net-pipeline-and-concurrency

RobV