I'm re-working some of my internal banner manager code and one thing I'd like to do is optimize the banner serving as much as possible. Banners are served from the database by default, but the banners can be cached optionally in memory so there's no data access.
Problem is though the banner click tracking still has to hit the database and while this isn't exactly slow it's slower and on occasion fires into some cleanup code. What I really want to do is something like this:
string output = "document.write(" +
wwWebUtils.EncodeJsString(banner.RenderLink()) +
");";
context.Response.Write(output);
Response.End();
// *** Update the hit counter - plus potentially other 'stuff'
// that's potentially slow
manager.BannerHit(banner.BannerId);
Of course you probably already know that this won't work because Response.End() REALLY means Response.End() as it throws an exception and finishes out the request so any code following a Response.End() code doesn't ever fire. BannerHit above never runs. Instead a ThreadAbort exception is fired which kills the current request.
There's also context.ApplicationInstance.CompleteRequest() which also closes out the request, but it doesn't properly shut down the Response object and so the request still hangs.
So, I started playing around with a few other ways to get this to work. One obvious though is to turn off Response buffering and just .Flush() the content when done:
HttpResponse Response = context.Response;
Response.Clear();
Response.BufferOutput = false;
Response.ContentType = "text/html";
string output = "document.write(" +
wwWebUtils.EncodeJsString(banner.RenderLink()) +
");";
context.Response.Write(output);
Response.Flush();
// *** Update the hit counter - plus potentially other 'stuff'
// that's potentially slow
manager.BannerHit(banner.BannerId);
By turning off Response buffering you're supposed to be able to flush content back to the client, but in some quick tests throwing in some Thread.Sleep() breaks I clearly failed to get the Response to flush output all the way to the client. It looks like the headers get sent with Chunked output headers provided, but it doesn't actually 'finish' the HTTP request so that the client knows the request has completed. Still doesn't work.
The first 'buffer' sent to the client (as captured with Fiddler) looks like this and includes only the headers:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
Expires: -1
Server: Microsoft-IIS/7.0
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET
Date: Sun, 25 May 2008 15:52:20 GMT
Connection: close
Then when the request finishes 5 seconds later I get the second chunk:
105
document.write("<a href='/WestWindWebToolkit/wwBanner.ashx?a=c&id=b628a170&t=633473275403436000&u=http%3a%2f%2frasnote%2fWestWindWebToolkit%2fBannerTest.aspx' target='_top'><img src='http://www.west-wind.com/banners/webconnectionBanner50.gif' border='0'></a>");
0
Both IE and Firefox wait until the final chunk arrives to actually display the data. So this makes this mechanism fairly worthless for small content. Both IE and FireFox render content eventually when the chunked data accumulated gets longer, but for small bits like the above nothing happens until there's enough data. Not sure if this is IIS holding off on sending back the chunked data or the browsers trying to cache the content first.
Really what I need to do is Response.End() but continue running my code in the current method. Looking at the Response.End code with Reflector yields:
public void End()
{
if (this._context.IsInCancellablePeriod)
{
InternalSecurityPermissions.ControlThread.Assert();
Thread.CurrentThread.Abort(new HttpApplication.CancelModuleException(false));
}
else if (!this._flushing)
{
this.Flush();
this._ended = true;
if (this._context.ApplicationInstance != null)
{
this._context.ApplicationInstance.CompleteRequest();
}
}
}
so it really looks like there's no clean way to do this.This clearly shows that ASP.NET is bascially throwing the entire thread, which is pretty brute force. The else block looks promising except that _ended isn't accessible. I'm not quite sure where the actual ThreadAbortException comes from as neither CancelModuleException() or the CompleteRequest fire it.
After searching around for a while I gave up and just did the obvious:
string output = "document.write(" +
wwWebUtils.EncodeJsString(banner.RenderLink()) +
");";
context.Response.Write(output);
// *** Ignore Response.End() exception
try
{
Response.End();
}
catch { }
Thread.Sleep(5000);
// *** Update the hit counter
manager.BannerHit(banner.BannerId);
and this surprisingly works! Given the code above and the CancelModuleException() signature and implementation I'm not even sure why this even works at all, but when run the above the banner HTML is immediately pushed out and rendered.
In this handler scenario I'm working with the behavior is probably fine because there won't be other modules or end handlers that need to fire. But inside of an ASP.NET page or other more complex handler I think I'd be worried about side effects for this behavior. For now this does the trick in this scenario.
So, what are you doing to handle an 'early exit' scenario in ASP.NET when you need additional processing? I suppose another option would be to fire an asynchronous delegate and let it go off on a separate thread, but that seems like overkill as well.
It seems there should be an easier solution to this possibly common scenario.
Other Posts you might also like