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 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.
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.
- 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.