9

To clarify on my question I've been developing an app that does a lot of database updates / web service calls based on the input from a user (using an excel spreadsheet). If there are a lot of updates to make the process can take in excess of 20 minutes to run.

To stop my UI from freezing / timing out I've been looking into multithreading so I can run my long running process in an asynchronous manner and in the mean time simply displaying an animated gif whilst the process runs.

This all seems to run nicely at the moment with my test data, but when I substitute in the actual long running process I get an error regarding HttpContext.Current.User.Identity.Name. I've read up on this and from this article1 I took it to mean that if you set the 'Async' property to 'true' in the page directive and used the RegisterAsyncTask method you could then access HttpContext.Current. However, for me this doesn't seem to be true. I'm sure it's something I'm doing, so here is my code (I've mainly been using the following articles to write this article2 and article3):

ASP.NET page

<%@ Page Title="Home Page" Async="true" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="false" CodeBehind="Index.aspx.cs" Inherits="MyApp.Index" %>

C# - RegisterAsyncTask is done on a button click, which starts the long running process:

protected void ProcessUpdates()
{
    //Register async task to allow the processing of valid updates to occurr in the background
    PageAsyncTask task = new PageAsyncTask(OnBegin, OnEnd, OnTimeOut, null);
    RegisterAsyncTask(task);

}

IAsyncResult OnBegin(Object sender, EventArgs e, AsyncCallback cb, object state)
{
    return Worker.BeginWork(cb, state);
}

private void OnEnd(IAsyncResult asyncResult)
{
    //UpdateResults list should now have been filled and can be used to fill the datagrid
    dgProcessedUpdates.DataSource = Worker.UpdateResults;
    dgProcessedUpdates.CurrentPageIndex = 0;
    dgProcessedUpdates.DataBind();

    lblProgress.Text = "Update Results: update success / failure is shown below";
}

private void OnTimeOut(IAsyncResult asyncResult)
{
    lblProgress.Text = "The process has timed out. Please check if any of the updates have been processed.";
}

C# - Worker class

public class Worker
{
    public static List<AuditResult> UpdateResults = new List<AuditResult>();
    private delegate void del();

    //This method is called when the thread is started
    public static IAsyncResult BeginWork(AsyncCallback cb, object state)
    {
        del processing = DoUpdateProcessing;
        return processing.BeginInvoke(cb, state);
    }

    private static void DoUpdateProcessing()
    {
        //UpdateResults = ExcelFileProcessing.PassValidUpdates();

        //Testing

        Thread.Sleep(5000);

        int i = 0;

        while(i < 10)
        {
            AuditResult ar = new AuditResult();
            ar.Result = "Successful";
            ar.JobNumber = (1000 + i).ToString();
            ar.NewValue = "Test New Value " + i.ToString();
            ar.ResultDate = DateTime.Now.ToString();
            ar.UserName = HttpContext.Current.User.Identity.Name;

            UpdateResults.Add(ar);
            i++;
        }
    }
}

Initially my test code didn't include a call to HttpContext.Current.User.Name for ar.UserName but after my issues with putting back in the call to ExcelFileProcessing.PassValidUpdates() with this I decided to do it. When I reach that part (ar.UserName = HttpContext.Current.User.Identity.Name) it says 'Object reference not set to an instance of an object', which suggests the HttpContext isn't carried across to the second thread. How can I do this?

UPDATE

I've currently reverted back to my previous code (that wasn't initially working) and simply passed the HttpContext.Current as a variable to my DoWork method as per this SO question like this:

Create 2nd thread

    protected void ProcessValidUpdates()
    {
        Worker workerObject = new Worker();
        HttpContext ctx = HttpContext.Current;
        Thread workerThread = new Thread(new ThreadStart(() =>
                                {
                                    HttpContext.Current = ctx;
                                    workerObject.DoWork();
                                }));

        workerThread.Start();

        //Loop until worker thread activates
        while (!workerThread.IsAlive) ;

        //Put main thread to sleep to allow the worker thread to do some work
        Thread.Sleep(1000);

        //Request the worker thread stop itself
        workerObject.RequestStop();

        //Use the Join method to block the current thread until the object's thread terminates
        workerThread.Join();

        //UpdateResults list should now have been filled and can be used to fill the datagrid
        dgProcessedUpdates.DataSource = Worker.UpdateResults;
        dgProcessedUpdates.CurrentPageIndex = 0;
        dgProcessedUpdates.DataBind();

        lblProgress.Text = "Update Results: update success / failure is shown below";
    }

Worker Class

public class Worker
{
    //volatile hints to the compiler that this data member will be accessed by multiple threads.
    private volatile bool _shouldStop;
    public static List<AuditResult> UpdateResults = new List<AuditResult>();

    //This method is called when the thread is started
    public void DoWork()
    {
        while (!_shouldStop)
        {
            //Testing
            Thread.Sleep(5000);

            int i = 0;

            while (i < 10)
            {
                AuditResult ar = new AuditResult();
                ar.Result = "Successful";
                ar.JobNumber = (1000 + i).ToString();
                ar.NewValue = "Test New Value " + i.ToString();
                ar.ResultDate = DateTime.Now.ToString();
                ar.UserName = HttpContext.Current.User.Identity.Name;

                UpdateResults.Add(ar);
                i++;
            }

        }
    }

    public void RequestStop()
    {
        _shouldStop = true;
    }
}

This seems to work in that I can now access HttpContext.Current and the username I expect. I think this is probably to some degree what some of you were proposing anyway. I appreciate the solution suggested by Andrew Morton but at the moment that would require a significant rewrite. At the moment my process already calls a web service to do the database stuff and returns a success or failure result. It also has to call another BPEL service directly. As such I suspect there may be further performance hits if I had to wrap all this into another web service. In addition, most calls to the process won't be that long running (probably less than 10 mins), so this is really only to address the few requests that exceed 20 mins. Finally, this is only likely to be used by 1 or 2 people, so it's unlikely to have a huge number of requests at 1 time.

However, bearing in mind my current solution, is there anything I should be aware of that might trip me up? IIS causing issues? Any additional help would be very much appreciated.

Community
  • 1
  • 1
sr28
  • 3,717
  • 4
  • 29
  • 49
  • What L.B said, although I'd pass the `User` directly, rather than the whole `HttpContext` - that might mean trouble, given that the request has already ended. In any case, this is *not* how `Async` works - you just have background work to do, not an asynchronous request. – Luaan Aug 06 '14 at 15:25
  • @Luaan - not sure I understand what mean by your last sentence. Are you saying that I shouldn't be using async like this? As for passing in the User would this be when I create the PageAsyncTask? Any chance of some code? Never touched async before so trying to muddle through it at the mo. – sr28 Aug 06 '14 at 15:30
  • You only need the user name - so pass the user name to the worker method that runs in background. And you should be mucking around with `Async` for this at all - that's there to handle asynchronous I/O operations (i.e. the async methods free up threads, rather than uselessly blocking them), not background worker threads. And the way you wrote this, it's not even asynchronous at all - you're just starting up a new thread and throwing away the old one rather than using the original one in the first place. In other words, "not helping at all". – Luaan Aug 06 '14 at 15:34
  • @Luaan - ok, so I've got the wrong end of the stick then. It sounds like I should be setting 'Async' back to 'false' as I've misunderstood its use. Thank you for that. As for not writing this to be asynchronously, could you point me in the right direction? – sr28 Aug 06 '14 at 15:39
  • @L.B - thanks for the input. Could you read the new comments and perhaps point me in the right direction? Thanks. – sr28 Aug 06 '14 at 15:40
  • @Luaan - I had code that was doing something similar to this article but I had the same problem: http://msdn.microsoft.com/en-us/library/7a2f3ay4(v=vs.90).aspx In your view would this be more appropriate for what I need (and I just pass the User.Identity.Name to the DoWork())? – sr28 Aug 06 '14 at 15:59
  • 2
    If you had a Windows service which performed the long-running process, you could have that service update a database table with some sort of user ID, task ID, and the current progress. Then the web page could use AJAX to query a web service which queries the database. The user could even start several lengthy processes, go to lunch, then log back in and see the progress of their tasks. That way, you are not at the mercy of IIS recycling something and breaking it. Also, you could have an overall workload page showing how much work is in progress/pending. – Andrew Morton Aug 06 '14 at 17:38
  • @sr28 In a way, except that it's a bad idea to spawn long running threads in ASP.NET, and the article is antiquated. Andrew Morton said the proper way to handle it, but if that's not possible for you, there's a few ways to handle this in ASP.NET. Just do understand that the task can be aborted at any time. – Luaan Aug 06 '14 at 18:43
  • @Luaan - thanks for looking at the article. I'm not really certain how you can do it in ASP.NET, which I'm sure is better designed. I'd appreciate you looking at my update and giving your opinion on it as a solution. – sr28 Aug 07 '14 at 08:02
  • @sr28 Well, I really wouldn't pass the whole `HttpContext`, you shouldn't really be messing with that, but whatever. Apart from that, just make sure that you're not going to end up in trouble when the application pool is restarted while the thread is in the middle of processing. However, it's hard to see what you've actually accomplished here. Why don't you just do the whole thing in-request while periodically writing the response and `Flush`ing the output? You can still keep the request going this way, and you're notifying the user too. – Luaan Aug 07 '14 at 12:01
  • @Luaan - I shouldn't have an issue with IIS restarts as this only occurs out of hours in the morning. With regards to your last couple of sentences are you suggesting just running 1 thread but breaking the process down into chunks and reporting the progress to the user? – sr28 Aug 07 '14 at 13:26
  • @sr28 Yeah, keep it in the original request's thread. As long as you keep enough data flowing to the browser, it will usually not time-out the request. I've used this before for very long operations. – Luaan Aug 08 '14 at 07:24
  • 1
    FYI I've found the HttpContext shouldn't be used in a multithreaded context, confirmed by Microsoft. It's crashing IIS under load. – Yuriy Faktorovich Aug 01 '16 at 14:34
  • @YuriyFaktorovich - thanks for noting that. In the context of this question I don't think that's a likely scenario due to the fairly low volume of requests. However, definitely worth noting for anyone else stumbling across this and hoping to use this as a solution. – sr28 Aug 01 '16 at 15:57

1 Answers1

2

I have a site on a shared server. I need to have a BATCH job and I do that in another thread. It can run up to 1 hour (I ping the site so the worker process does not stop).

I went down the road of tying to get the current context. After many hours of research and searching it cannot be done. In a new thread the httpcontent.current is not there, it is not the same thread as the user was accessing, so the context did not carry over, and you cannot access the logged in user, since they are not logged into that thread.

Patrick
  • 1,545
  • 12
  • 20