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("xyz");
if(!doc.Locked){
// other code
}
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.