Ayende @ Rahien

Unnatural acts on source code

Challenge: Find the bug fixes

In the following code there are two very subtle bug fixes that result from painful experience. Can you spot them?

public class Global: System.Web.HttpApplication
{
	public static ISessionFactory SessionFactory = CreateSessionFactory();
	
	protected static ISessionFactory CreateSessionFactory()
	{
		return new Configuration()
			.Configure(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "hibernate.cfg.xml"))
			.BuildSessionFactory();
	}
	
	public static ISession CurrentSession
	{
		get{ return (ISession)HttpContext.Current.Items["current.session"]; }
		set { HttpContext.Current.Items["current.session"] = value; }
	}
	
	protected Global()
	{
		BeginRequest += delegate
		{
			CurrentSession = SessionFactory.OpenSession();
		};
		EndRequest += delegate
		{
			if(CurrentSession != null)
				CurrentSession.Dispose();
		};
	}
}

Oh, and it isn’t a Null Reference Exception in the EndRequest event handler.

Comments

Ryan
08/06/2009 08:39 PM by
Ryan

HttpContext.Current is thread safe where a static member is not?

Diego Mijelshon
08/06/2009 08:47 PM by
Diego Mijelshon

Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "hibernate.cfg.xml") looks like one...

It was probably the only "safe" way to get that path.

Johannes Rudolph
08/06/2009 08:49 PM by
Johannes Rudolph

On of them might be using AppDomain.CurrentDomain.BaseDirectory to get the path of the executing assembly instead of Assembly.GetAssembly() or Assembly.GetEntyAssembly() and then figuring out the path based on their location.

I have hit a problem like that while unit testing a winforms app (entry< assembly was different of course, stupid me :-) ).

Survivor_Zero
08/06/2009 08:53 PM by
Survivor_Zero

Why not just use CurrentSessionContext.Bind(..) and .Unbind(..) ?

Huseyin Tufekcilerli
08/06/2009 08:56 PM by
Huseyin Tufekcilerli

You have changed this line:

.Configure(Path.GetFullPath(@".\hibernate.cfg.xml"))

to this:

.Configure(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "hibernate.cfg.xml"))

anon
08/06/2009 09:08 PM by
anon

also the if(CurrentSession != null)

Tuna Toksoz
08/06/2009 09:18 PM by
Tuna Toksoz

Putting things into constructor instead of application_start? don't know...

Tuna Toksoz
08/06/2009 09:22 PM by
Tuna Toksoz

Ouch, ignore what I said. It is not a constructor.

VirtualStaticVoid
08/06/2009 09:27 PM by
VirtualStaticVoid

One problem has to be a cast problem in the CurrentSession property getter:

return (ISession)HttpContext.Current.Items["current.session"];

This should be written as:

return HttpContext.Current.Items["current.session"] as ISession;

Neal Blomfield
08/06/2009 09:50 PM by
Neal Blomfield

Your subtle bugs are enlightening and infuriating at the same time. Aside from the code to determine the path of the config file, nothing else seems like it could cause a problem - which means I'm missing a key understanding =(

@VirtualStaticVoid

You are wrapping your code in cotton wool. The code explicitly puts an ISession instance into the context at that key and therefore it is more than reasonable to expect an ISession instance to be retrieved from the same key.

Your solution also creates a nasty problem in that it hides the true error by causing a null ref exception at the first place the session is used rather than an invalid cast exception at the point the error could occur.

Frank Quednau
08/06/2009 09:50 PM by
Frank Quednau

Why is "CreateSessionFactory" protected?

Stephen
08/06/2009 10:20 PM by
Stephen

I guess technically you could try and get the session before the BeginRequest has fired (or fired at all if it isn't a http thread).. and you assume a http context.. but those are things you could well accept if you dont spawn any other threads or try and use the session before the request as started (although perhaps ioc resolution happens before that).

Rafal
08/06/2009 10:29 PM by
Rafal

I have no idea what the problems were, but I would assume it could be

creating the session factory in Application_start or on first use instead of static constructor

Andy Pook
08/06/2009 10:32 PM by
Andy Pook

The Global method where the events get bound will never get called. It seems that it should be an instance ctor so that they get bound when the app starts.

Stephen
08/06/2009 10:40 PM by
Stephen

Andy, there are multiple HttpApplication instances running on a website, they act almost as 'runners', and return to a pool where a set amount of them is kept alive, the ctor should run (wouldn't be subtle if it didnt I guess ;)).

Daniel
08/06/2009 10:45 PM by
Daniel

One's got to be the Path.Combine, which is good practice any time you build up a path from multiple parts, since simple string concats can fail if slashes, etc. are/aren't provided when expected.

The second I'm not sure about, but I'd guess using AppDomain.CurrentDomain.BaseDirectory? I've seen a lot of code that just used "\" or similar, not taking into account that apps may be published in something besides a root directory.

Franck
08/06/2009 11:25 PM by
Franck

When the protected Global is called ? I guess it is never called so the session is never created ?

Rob
08/07/2009 12:26 AM by
Rob

public static ISessionFactory SessionFactory = CreateSessionFactory();

instead of a static constructor?

Hendry Luk
08/07/2009 02:07 AM by
Hendry Luk

I think you might have changed CurrentSession.Close() into CurrentSession.Dispose().

It might have something to do with TransactionScope. With Session.Close(), NH may attempt to disconnect the connection while a transaction is still in progress... resulting in an ocassional exception.

Mike Brown
08/07/2009 02:57 AM by
Mike Brown

Off the top of my head, I'd guess that storing the CurrentSession in the request instead of the Session is a bug fix. One might assume that the session is safe because it'll get refreshed with each request. But then what happens with AJAX requests. Suddenly, your session drops from beneath you because of a concurrent dispatch.

Hendry Luk
08/07/2009 03:14 AM by
Hendry Luk

The 2nd bug fix... just a guess.. but I think you changed NHinitialization from applicationstart (or init) into a static method. This is related to an issue in IIS7 where you can't initialize NH during applicationstart or init.

Ayende Rahien
08/07/2009 06:53 AM by
Ayende Rahien

Diego & Johanas

I actually forgot about the BaseDirectory one, yes, that is certainly a common bug

Ayende Rahien
08/07/2009 06:53 AM by
Ayende Rahien

Tuna,

Ctor is definitely a biggie, now tell me why.

Ayende Rahien
08/07/2009 06:54 AM by
Ayende Rahien

Virtual,

Nope, it is not a bug. If you put something other than ISession there, it is your own damn fault.

Ayende Rahien
08/07/2009 06:55 AM by
Ayende Rahien

Neal,

Look for something that isn't a problem. This isn't spotting t6he bug, this is spotting the FIX

Ayende Rahien
08/07/2009 06:56 AM by
Ayende Rahien

Frank,

No reason, it can be private as well.

Ayende Rahien
08/07/2009 06:58 AM by
Ayende Rahien

Andy,

Very close, but you haven't explained why it will never get called or why it should be instance ctor

Ayende Rahien
08/07/2009 07:00 AM by
Ayende Rahien

Stephen,

YES!

You got one of the problems, the event wiring must happen in the ctor.

Application_Start, where I have seen a lot of people put them is going to cause problems when you have concurrent requests

Ayende Rahien
08/07/2009 07:01 AM by
Ayende Rahien

Rafal & Rob,

Yes, but can you explain why?

Ayende Rahien
08/07/2009 07:03 AM by
Ayende Rahien

Mike,

You really want to avoid putting the NH session in the ASP.Net session.

No, that is not it.

And sessions force serialization of requests anyway

Ayende Rahien
08/07/2009 07:04 AM by
Ayende Rahien

Hendry,

You are correct about the change, but not about why I did it. I am not aware of the issue you brought up.

Rafal
08/07/2009 07:57 AM by
Rafal

I think it's because there can be more than one Global instance.

Hendry Luk
08/07/2009 08:17 AM by
Hendry Luk

I am not aware of the issue you brought up.

Not sure if this is a problem in NH, but we had that problem in SharpArchitecture (or rather Fluent-NH) with IIS7 where NHibernate.Init() crashes when called in application_start.

code.google.com/.../detail

So they changed it to singleton-method call in each Application_BeginRequest:

NHibernateInitializer.Instance().InitializeNHibernateOnce(

            () => NHibernateSession.Init(etc etc));

Anyway, maybe you changed it because you want it invoked only once... And we can't guarantee application_start to be called once in a lifeatime of an application, particularly in the case of application-pool recycling.

Gareth
08/07/2009 08:38 AM by
Gareth

Do you need to set SessionFactory to null when the application ends to avoid memory leaks?

Tuna Toksoz
08/07/2009 08:46 AM by
Tuna Toksoz

ApplicationStart is not viable as many instances of HttpApplication is created, the later ones will not benefit from this. Init may be used for that purpose, instead of ApplicationStart.

protected void Global() is not a ctor and this made confused :)

Stephen
08/07/2009 09:01 AM by
Stephen

The only thing I can think of is that perhaps the static init could silently fail, causing you to wonder why you don't have a session factory for subsequent requests.

Stephen
08/07/2009 09:04 AM by
Stephen

Meh, scratch that, aparently static init will def~ happen in context of the first request.. maybe related to static field init ordering?

Peter Morris
08/07/2009 09:05 AM by
Peter Morris

I think it is impossible to look at working code and guess what the error was in the old code that you have not seen. There are millions of possibilities.

For example, I think the code was here...

You took out the NotImplementedException in CreateSessionFactory()

:-)

Tuna Toksoz
08/07/2009 10:06 AM by
Tuna Toksoz

Stephen,

I thought the same, the silent fail thing,it happened to me several times during dev, I had to catch the exception in the debugger to find the problem.

Shyam
08/07/2009 02:05 PM by
Shyam

I think the "bug" that I can spot, is that you are creating a session factory each time there is a webrequest rather than caching up the session factory in a static reference and checking to see if ts null or not.

gunteman
08/07/2009 02:20 PM by
gunteman

No, the session factory is only created once.

Ayende Rahien
08/07/2009 02:54 PM by
Ayende Rahien

Stephen,

Silently failing is the key, but static init will NOT silently fail

Ayende Rahien
08/07/2009 02:55 PM by
Ayende Rahien

Peter,

NotImplementedException is not subtle

Ayende Rahien
08/07/2009 02:57 PM by
Ayende Rahien

Tuna,

Opps!

protected void Global is a typo ;-) Fixed

And yep, the event wiring must happen for every instance of http app.

Stephen
08/07/2009 03:09 PM by
Stephen

OK, so.. is it that when running in classic mode (or IIS 6 and under), Application_Start will cause an exception only on the first request, but the next requests will all run and have no session factory (confusing to debug).. and the static initialization will throw every time a request tries to fire, thus making it easier to see what went wrong.

(Note that I think IIS7's default integrated mode now ensures the same for app_start).

Mike Brown
08/07/2009 05:04 PM by
Mike Brown

Request isn't available for App_Start in IIS7

Also, you have a potential race condition under appstart if multiple requests come in. I'd assume however that IIS serializes/queues further requests until appstart completes. IF it doesn't though...none of the requests that came before App_Start completed will have the SessionFactory initialized.

ernest
08/07/2009 09:07 PM by
ernest

ASP.NET uses a pool of HttpApplication instances, however ApplicationStart() method is called only on one of them. So when you bind events in the ApplicationStart() that the only one will have wired events and the others won't have wired events.

That is why you must use Global ctor.

zihotki
08/07/2009 10:04 PM by
zihotki

You are using HttpContext.Current instead of [ThreadStatic] ISession property - this will prevent from various bugs that related to IHttpAsyncHandler.

Event handlers added in ctor will be fired before handlers registered via Global.asax (ApplicationBeginRequest method, ApplicationEndRequest and so on) and before handlers of modules. And, as I think, this is the only reason why you used this approach.

This is all I can see from this code at the moment.

zihotki
08/08/2009 12:42 AM by
zihotki

And also you are using CurrentSession.Dispose() instead of CurrentSession.Close() because dispose method contains logic for logging of a session disposal. If you use Close method at the end of request information about session disposal will be logged only on run of GC. So the statistics will be wrong.

Niclas Pehrsson
08/16/2009 08:32 PM by
Niclas Pehrsson

I seeing one issue more if you will use asp.net mvc BeginRequest will be called when you getting .css, jpeg.... files. And the application will open one session for each image or style file loaded.

It would be smart to only open it for aspx and other file extensions which will use nhibernate.

Stephen
08/17/2009 02:21 PM by
Stephen

@Niclas, this isn't specifically an mvc thing.. whatever requests you map IIS to handle (and with IIS7 this is even more so) will get sessions.. you would potentially filter by the handler type, not the extension which is unreliable.. an easier system which has other benefits is to move such static content onto another 'dump' domain, so you can cut the request / response down to an absolute min.

Stephen
08/19/2009 07:39 PM by
Stephen

One thing I was wondering ayende, was why you chose to store the nhibernate session in in the http session, couldn't you just have an instance variable, and assign to it on begin request and dispose of it on end request. I'm sure IIS ensures that a given HttpApplication can only handle one request at a time (sounds obvious but async handlers could mean this isn't so).. is it purely so you can use the static helper property to find the session?

Ayende Rahien
08/19/2009 08:11 PM by
Ayende Rahien

Stephen,

I am not putting it in the http sesson, I am putting it on the http request

Stephen
08/19/2009 08:50 PM by
Stephen

Ah yes, completely misread the code.. I always assume Session when I see default property access with httpcontexts.

Apologies!

Comments have been closed on this topic.