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:
- Allow unexpected exceptions on all threads to be logged by a top level exception handler
- Do not let the application die because of an exception, no matter which thread it is running on (at least not before I get a chance to log it).
- Do not clutter the application logic with repeated try..catch and logging 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) { }
}
}
}
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);
}
}
<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?
Comments
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.