Wednesday, February 27, 2013

RavenDB is a bad choice for Asp.Net Session Provider

I was considering using RavenDb as a custom session state provider in a web application.  Essentially I am looking for a document database that works as a session state provider and also serves other application needs.  NServiceBus bundles this database, so I thought I don't have to use one more document database.

This is my first encounter with RavenDb.  I read lot of good things about this database.  I wanted give a try to see if this fits in to my requirements.

With a little bit of googling,  you can find few session state provider implementation for RavenDb here, and here.  All these implementations are based on Microsoft's ODBC session state provider sample here.

All the important methods you need to implement for your own provider are explained here.

Session State Provider is little tricky to implement.  In the context of session asp.net can server three types of page requests.
  • Pages that require no session
  • Pages that require read-only session
  • Pages that require writable session
All these requests can concurrently reach your provider.  Thanks to ajax calls.
All these three types of calls have the potential to update the session document concurrently.

Now the writable session pages attempt to use 'LockId' and 'Locked' properties to protect session data corruption from concurrent write calls.

Pages that require no session data just extend the expiry time of the session.  Pages that required read-only sessions should be able to retrieve the session data given a session id and they will wait for the releasing lock.

All of the above RavenDb session state provider implementations buckle at concurrent user loads.  These providers try to work around RavendDb limitations for this scenario.

Queries require Indexes

You cannot use RavenDb queries to get the session document(s).  This is because RavenDb queries require indexes and these indexes run in the back ground.  Under a heavy load these indexing yields stale documents. And if you wait for the indexing to finish you will run in to timeouts.

No Find And Modify Support

RavenDb won't support find and modify operations over a collection.  Following type of query is not possible.  Now you are left with loading the session by its id and then modifying its partsby examining its properties.

UPDATE Sessions SET Locked = true WHERE Id = 'xyz' and Locked = false

With out using indexes, and without the support for atomic operations as above, you are forced to write the following code.


var doc = sessionStateDoc.Load<SessionState>("xyz");
if(!doc.Locked){
// other code
}

This kid of code increases the chances for concurrency.

Optimistic Concurrency

Pages that doesn't requie session attempts update the "Expires" property of session document, at the same time a page that requires writable session might attempt to update the "LockId" property of session document.  

Even though they are totally different properties of the same document, we are forced to deal with the concurrency.  There might be a way to do fine grained concurrency based on specific fields, I find it way too much trouble than necessary.

When session module calls GetItemExclusive method, if it runs in to concurrency problems, we can simply return null by indicating session module to retry getting the document.  But how many times we should do this?  This slows down the pages.

You can probably attempt to use RavenDb patch command update "Expires" property, thus avoiding concurrency conflicts.  But soon you will find that this idea fails when we try to use RavenDb Expires Bundle.

Expiring documents

RavenDb comes with an expiration bundle that allows you to remove expired session documents.  In order to make this bundle work, you need to make use of the metadata constructs like the following. Unfortunately you must include this setter as part of the unit of work.  

db.Advanced.GetMetadataFor(session)["Raven-Expiration-Date"] = DateTime.UtcNow.AddMinutes(20)

sessionStateDoc.Expires = DateTime.UtcNow.AddMinutes(20)
sessionStateDoc.SaveChanges();

This prevents us from doing partial updates to the document. This metata data update must be done every time you update the collection. 

Other minor but annoying issues  
  • As of build #2261 there are still bugs. 
  • Master-Master replication won't work when you use API Keys.  
  • Expiration bundle randomly deletes session documents.  
  • Raven Studio doesn't give you a comfortable feeling of using a professional grade database.
Following changes gave me a relatively stable implementation of RavenDb Session Provider under higher loads.
  • Do not use expiration bundle.  Use a server side trigger or a scheduled task to expire documents. This allows us to do path command for updating "Expires" property in "ResetTimeOut" method. 
  • Do not use concurrency checks while removing the item, saving the session data, and while releasing exclusive locks.  These calls must succeed, if they fail you might get in to logic errors in the app. 
  • Use optimistic concurrency check only in GetItemExclusive/GetItem routines.  If the concurrency check fails, simply return null, this will force session module make calls to these methods.
All in all I am not happy about the friction.  I smell maintenance head aches.

I would like to try another document database to see if that fits better for this usecase.