Back last year, I outlined a problem I had with calling Response.End from a thread other than the thread that handled the request. It stemmed from a progress mechanism we had implemented that had the ability to cancel long-running tasks when the user hit stop or closed the browser. As it turns out, that wasn't the whole story.
We recently added a few reports where, the majority of the time, gathering the data takes over a couple of minutes. While doing memory and performance optimizations, we noticed our worker process was recycling with the following message in the System event log:
A process serving application pool 'XXXXXXXXXX' terminated unexpectedly. The process id was '####'. The process exit code was '0xff'.
Googling for this error doesn't do you much good since it could be caused by a variety of reasons. Most of the advice I came across wasn't well thought out and made quite a few bad assumptions. So we added some more debug logging and determined that this was happening when our canceling mechanism kicked in when a user decided they didn't really want to wait 5 minutes for the report. I was greatly puzzled by this since this feature had been tested thoroughly and had been running in production for some time. Looking back through the server logs, it was evident that it had been happening all along, just not very often. We had just gotten the performance on the vast majority of pages to be very good and it wasn't an issue. The problem only came to the surface when we added the report that always takes a while.
Here's the problem. If the handling thread is aborted, it causes a condition that IIS considers to be bad and that forces the worker process to recycle. (presumably, there is some communication that doesn't occur) This doesn't happen when Response.End() is called because it passes a special exception as the exception state to Thread.Abort. The HttpApplication catches ThreadAbortException and checks the ExceptionState. If it is an HttpApplication.CancelModuleException, it knows there was either a timeout, or Response.End() was called, and it cancels the Thread.Abort by calling ResetAbort which allows the thread to continue running at that point. I thought that was pretty slick.
When I was aborting the handling thread, I was on a different thread, so I had to call Thread.Abort manually, so CancelModuleException was not being used, so the thread was ending completely and causing the recycle. Since HttpApplication.CancelModuleException is internal (and rightly so) I could not simply use that mechanism.
The good news is that the Unload event always happens (for all practical purposes), even when the thread is being aborted. So I added my own PageIsCancelling property to our base class Page and check it, along with the current ThreadState in Unload, and cancel any pending abort if the page is canceling. So, the abort is contained within the callstack of the page, and the thread stays alive and all is well. No more crazy recycling.
As an aside, it seems this is aggravated by multiple processors, which might explain how it passed testing on the dev's machine. Although I don't have any proof of this.
Remember Me
Page rendered at Thursday, August 21, 2008 8:59:02 PM (Pacific Standard Time, UTC-08:00)
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.