10

I'm building a Logging library that stores everything on an Azure table. Writing to that table obviously takes a lot of time (never more than 1 sec, but it's still too much to make the user wait), so Log method returns a LogResult instance, here's the class

public class LogResult
{
    public string Id { get; set; }
    public Task LoggingTask { get; set; }

    public LogResult(string id, Task task)
    {
        Id = id;
        LoggingTask = task;
    }
}

And here is how the Log method finishes

return new LogResult(id, Task.Factory.StartNew(() => 
    DoLogInAzure(account, id, exception, request))
);

To give the caller the option of waiting for it to complete (if it's a console app, for instance). The issue I'm facing is that IIS shouldn't wait for it before returning the user the response... and if I don't wait for it, IIS doesn't always execute the task. The idea is to show the user a message "... If you contact us, be sure to mention your issue number, XXX" and don't make him wait until the log entry has been written.

Is there any way to force IIS to wait until the task finishes, even after it returned the response? I'm thinking I may need to code a Windows Service that takes the request asynchronously, but it looks like a lot of work just to add a log entry... specially if I can force IIS to wait for it.

Thanks for any ideas!

g3rv4
  • 19,750
  • 4
  • 36
  • 58
  • I've heard that IIS restarts 'websites' that take too much memory... – Sidharth Mudgal Sep 30 '12 at 22:08
  • I understand :) however, I don't think that's my case, as if I wait for the task to end, IIS finishes the execution and the log entry is written 100% of the times – g3rv4 Sep 30 '12 at 22:10
  • As an alternative, could you just use an azure queue to log to. Then a separate message handler can asynchronously write to the azure table. Better yet, if you get an error processing the message remains on the queue and will be processed again later. – Chris Chilvers Oct 01 '12 at 00:40
  • @usr: the Id can be either a Guid or an incremental number. If it's a Guid, the generation takes no time, the logger returns the Id and then stores the data in the background. I do need to show the user the Id. – g3rv4 Oct 01 '12 at 01:20
  • @ChrisChilvers yep... A windows service or a worker role do the trick, but really... The more components an app has, the more things you need to update on a new release, the more things will go wrong... If there's no other way, I'll def go that road... But I'd like to keep that as a last resort – g3rv4 Oct 01 '12 at 01:23
  • Any clues on this http://stackoverflow.com/questions/21583232/prevent-iis-from-killing-a-task-before-it-ends-part-2 ? – user3276151 Feb 05 '14 at 16:47
  • @user3276151 not really... but good luck :) – g3rv4 Feb 05 '14 at 17:32

4 Answers4

12

This post from Phil Hack talks about running background tasks in an ASP.NET application.

Damian Schenkelman
  • 3,505
  • 1
  • 15
  • 19
  • I've just finish reading that article... Thanks, it's really something I'm going to need. However, it doesn't explain why my task is not always executed... I'm running it on my machine, and generating 100 exceptions in a minute stores about 92 if I don't wait for the task to complete. The app pool is not restarted either... Does that even make sense? – g3rv4 Oct 01 '12 at 01:18
  • How are you generating the exceptions? When you wait, you just freeze the running thread until all Tasks complete, but that should not be necessary if the process is still running. Is the process stopping before the tasks are actually run? Use mock logic (e.g.: printing to debug window) in each task to make sure that all tasks had time to start and to complete. – Damian Schenkelman Oct 01 '12 at 14:21
  • The exception I'm dealing with right now, is b/c I'm assigning null on a non nullable string on an Entity... then, the `context.SaveChanges();` fails. However, if I generate an exception and don't do anything else, it's always stored. The way to make it fail is generating a lot of exceptions in a short period of time. I'm guessing that IIS may reuse the thread to handle new requests, and that's when my task is aborted. I'm doing 100 requests (which throw an exception) in less than 10 seconds, and that's when it fails. – g3rv4 Oct 01 '12 at 21:38
  • Thanks Damian!!! I think I was correct on my guessing... It looks like IIS was reusing the thread to handle incoming requests (I did a lot of them in a short term) and then, the thread taking care of my task was being terminated. Using `HostingEnvironment.RegisterObject()` I've been able to notify IIS I'm doing some work on that thread and now I don't loose even a single log entry. Thanks, you've solved an issue that has a big impact on a lot of projects I've worked on. Thanks – g3rv4 Oct 02 '12 at 22:22
10

Thanks to Damian Schenkelman and that blog post of Phil Haack, I figured out the problem and the solution. The problem is that IIS reuses the threads when it needs to handle new requests. And as it doesn't know that my task is doing some work, it reuses that thread (which makes sense). Then, I just have to notify IIS that I'm using that thread and that it can't be reused (so, it has to either reuse another thread, create a new one, or make it wait). I ended up using my own TaskFactory that handles the task creation, and automatically registers a notifier in IIS. For completeness, to help some other folk with the same issue as me, and to read another suggestions, here's what I've done

public class IISNotifier : IRegisteredObject
{
    public void Stop(bool immediate)
    {
        // do nothing, I only run tasks if I know that they won't
        // take more than a few seconds.
    }

    public void Started()
    {
        HostingEnvironment.RegisterObject(this);
    }

    public void Finished()
    {
        HostingEnvironment.UnregisterObject(this);
    }
}

And then

public class IISTaskFactory
{
    public static Task StartNew(Action act)
    {
        IISNotifier notif = new IISNotifier();
        notif.Started();
        return Task.Factory.StartNew(() => {
            act.Invoke();
            notif.Finished();
        });
    }
}

Now, when I want to start a the log task I just do

return new LogResult(id, IISTaskFactory.StartNew(() => 
    DoLogInAzure(account, id, exception, request))
);

You can see (and download the code) at https://github.com/gmc-dev/IISTask

g3rv4
  • 19,750
  • 4
  • 36
  • 58
0

The information is not enough but I suspect it may be related to GC and references if it works when you wait fo the task. For your purpose, a better way is to use ETW (EventProvider) and set the ActivityId for each request. Simply configure an ETW session can redirect all the messages to a file. You can show the ActivityId (a Guid) to the end user.

lidali
  • 1
  • 1
  • 2
    `Task` will run fine even if there is no reference to it, that's not the problem. – svick Oct 01 '12 at 08:28
  • In order to check if that's the issue, I stored all the references to the `Task` in a singleton instance... however, that didn't change its behavior... So far, the only thing that works, is waiting for the task before the response is retrieved – g3rv4 Oct 01 '12 at 11:28
0

Sorry for not adding this as a comment, I don't have enough rep.

https://msdn.microsoft.com/en-us/library/system.web.hosting.iregisteredobject(v=vs.110).aspx

Applications can have only one instance of a registered type.

This seems to indicate that Gervasio Marchand's accepted answer is somewhat incorrect, as each call to his static helper method creates a new IISNotifier, which is an IRegisteredObject.

joelises
  • 85
  • 5
  • 1
    were you able to find an example where this solution doesn't work? I would love to see something like that, as I've used this approach on tons of projects and never saw anything weird – g3rv4 Jun 18 '15 at 16:15