Article Index

Safely running background threads in ASP.NET 2.0

The .NET Framework 2.0 no longer allows background threads to die silently when an unhandled exception is thrown. I'm not going to dive into the details of the change, as it is already well-documented on the web (this post describes the behavior for different application types & Scott Allen focuses on the ASP.NET impact).

The impact is more significant for ASP.NET applications
I think this is actually a bigger change for ASP.NET apps, because it has always had a "safety net". In console/WinForms applications, if there is an unhandled exception on the primary thread, the application dies. Having your application die now because of an unhandled exception on a background thread is not that big of a difference. You have an unhandled exception in your application, the application dies. Consistent.

However, in ASP.NET, exceptions that you did not handle in your own code on the primary thread were always caught by a default handler (affectionately known as the "yellow screen of death"). Your application did not die. You could add a handler for the HttpApplication.Error event (Application_Error in global.asax) to centralize your exception handling and logging logic. In 1.x, exceptions that occurred on a background thread would die silently. Your application did not die. Consistent. But now, in 2.0, exceptions on your primary thread still do not kill your application, and can be logged by your global handler. But exceptions on your background threads will kill your application, and will not be logged.

My approach to exception handling and how it is now more difficult to implement
My philosophy is to never catch exceptions unless there is something I can do to address the issue. I prefer not to catch all possible exceptions throughout the internals of my code, and instead rely on try...finally blocks to clean up in case of an exception, and allow the exception to bubble up to a top-level handler where it can be logged (of course I catch exceptions when there is something the code can do to resolve the issue or continue in a known state).

The new background thread exception behavior forces me to create "top level handler and logging" code for the application (global.asax) AND within every method that I run on a background thread. I end up with a lot of duplicate code, and explicit exception handling and logging that I would prefer to be hidden behind the scenes instead of cluttering up my application logic.

A proposed solution
My goals:
To satisfy the top level exception handler goal, I would like to make use of the existing HttpApplication.Error / Application_Error mechanism. That means I need to somehow transfer the exception that occurred on a background thread to a primary ASP.NET thread. To satisfy the "do not die" and "do not clutter" goals, I need to wrap all calls to background thread methods with a single piece of code.

I've created the SafeWaitCallback class. It's Call method is used in place of a WaitCallback delegate. For example, if I want to execute the method DoTask on a background thread, I would call it like this:

System.Threading.ThreadPool.QueueUserWorkItem(new SafeWaitCallback().Call(DoTask));

SafeWaitCallback does two things: it wraps calls to the target method in a try...catch block, and it forwards the caught exception to a custom HttpHandler. It forwards the exception by serializing it to a byte array, and then POSTing it to the handler, which will receive the bytes and deserialize back to an Exception object.

using System;
using System.Net;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;

public class SafeWaitCallback {
  public static Uri ApplicationUri;
  System.Threading.WaitCallback callback;

  public System.Threading.WaitCallback Call(System.Threading.WaitCallback callback) {
    this.callback = callback;
    return CallbackWrapper;
  }

  private void CallbackWrapper(object state) {
    try
    {
      callback(state);
    }
    catch (Exception e)
    {
      byte[] exceptionData;

      MemoryStream stream = new MemoryStream();
      BinaryFormatter formatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Persistence));
      formatter.Serialize(stream, e);
      exceptionData = stream.ToArray();

      WebClient client = new WebClient();
      Uri handler = new Uri(ApplicationUri, "TransferException.axd");
      try
      {
        client.UploadData(handler, exceptionData);
      }
      catch (WebException) { }
    }
  }
}

By forwarding the exception to the custom HttpHandler, I can re-raise the exception, and since the handler will be running on a primary ASP.NET thread (not in the background), the exception will be caught by my global handler, and logged just like any other exception.

using System;
using System.Web;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;

public class TransferredExceptionHandler : IHttpHandler {
  public bool IsReusable { get { return true; }}

  public void ProcessRequest(HttpContext context) {
    byte[] exceptionData = new byte[context.Request.ContentLength];
    context.Request.InputStream.Read(exceptionData, 0, exceptionData.Length);

    Exception transferredException;
    MemoryStream stream = new MemoryStream(exceptionData);
    BinaryFormatter formatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Persistence));
    transferredException = (Exception)formatter.Deserialize(stream);
    throw new Exception("[Background exception transferred - see InnerException]", transferredException);
  }
}

The handler is registered in web.config with the following entry:
      <httpHandlers>
        <add verb="POST" path="TransferException.axd" type="TransferredExceptionHandler" />
      </httpHandlers>

Let me know what you think
I've attached the full source code in the form of a Web Site project. It includes a Default.aspx page which demonstrates the different scenarios of an unhandled exception (primary thread, background thread, "safe" background thread using my new code).

This is not production ready code, it is still in the proof of concept stage. For one, it probably needs better exception handling within the CallbackWrapper exception handler (what if the serialization blows up?). More importantly, I'm not yet convinced this whole idea is a worthwhile approach. It meets my goals, but it feels a little hackish. I wanted to see if it was possible, and then throw it out there to get some feedback. Anyone see any major flaws in the concept? Even better, does anyone know of a more elegant approach to achieve the same goals?

Download AspNetBackgroundExceptions.zip (2.55 KB)

Comments

Awesome! I like the idea a lot (and have blogged on it). A couple thoughts occur to me. First, do we capture enough details in the serialized exception? Maybe we should wrap another specific exception type around the actual caught exception with details about the thread where it really occured... I'm going to play some tomorrow. Second, maybe you should be doing a POST instead of a GET to insure we've got enough message-size available.
Marc Brooks - June 22, 2006 11:16pm
Thanks for the feedback Marc (and nice words on your blog http://musingmarc.blogspot.com/2006/06/more-about-exception-handling-and-new_23.html).

I am doing a POST, and not a GET. In fact, the handler is specifically registered only for the POST method - a GET attempt will give you a 404. I want to make it hard for people to call the handler directly. Along those lines, I'd like to add some code so that the handler only proceeds if the request is coming from the localhost. You don't want outsiders posting junk to your logs.

I originally planned to make a custom wrapper exception type (BackgroundThreadException?) in order to gather more details, but when it came down to it, I couldn't find a use for it. I was going to capture the thread name, but then realized that since I'm using a ThreadPool thread, the thread identity really doesn't mean anything (as the thread can be reused for multiple tasks). Of course, you could expand on the library to add wrappers for ThreadStart and ParameterizedThreadStart delegates, in which case knowing the thread identity could become more useful.

Let me know if there are additional details you can think of that would be useful to capture. I appreciate the feedback in fleshing this thing out into a useful tool.
Joshua Flanagan - June 23, 2006 06:30pm
Hi. I liked the idea. Iim trying it out but I get 401 Authorize error when I call the uploaddata method on the webclient. Is there any config on IIS that has to be done?
Jens Alenius - January 25, 2008 01:32am