The 15 Factors of ASP.NET — Stateless Processes
In my book, Beyond the 12 Factor Application, one of the factors that I talk about in general terms is the concept of a stateless process. One of the most common questions I get from people when discussing this topic is, “How can anything ever be truly stateless?”
The idea behind the question is that just about every application, regardless of business domain or vertical, needs to store data somehow and stored data is state. So, how can we make stateless processes and how does this apply to ASP.NET applications?
The term stateless is a little misleading. It doesn’t imply that a service cannot have the appearance of state, it simply means that the state cannot be internal to the service for a duration longer than a single request.
Put another way, if successive requests from the same client are handled by two different and isolated instances of your application, and this could potentially cause request failure or data corruption, your service is not stateless.
Session State
ASP.NET developers have a long and storied history with session state. For a user-facing web application, session state really just means any information that persists for a user between requests. In a broader sense, this can be accomplished through the use of cookies, repositories, or both.
By default, ASP.NET sessions are stored in memory. This is absolutely a violation of the stateless rule. Thankfully it is pretty easy to externalize state management by configuring session state something like this:
<sessionState
mode="SQLServer"
cookieless="true"
sqlConnectionString="user id=blah;server=blah; etc; etc;" />
This is all pretty easy and straightforward and you can ask yourself a very specific question — Is my session state out-of-process? If it is, your sessions are compatible with running in the cloud. If it isn’t, you’ll need to externalize your session state, either with the SQL session state provider or with another provider for a different data source.
Application State
Where session state is easy, straightforward, and should take very little time at all to externalize, application state is far more tricky. Every variable that your application reads or writes outside the scope of a single request can be considered application state.
This also means that every object that resides outside request scope should be considered suspect and needs to be evaluated on the following criteria:
- Could a change in this data effect the results of subsequent requests?
- If two different instances of your application are running and they receive subsequent requests from the same client, will the client receive confusing or corrupted results?
- If one of your application instances restarts between requests, will clients receive bad (confusing, corrupted, outright wrong) results?
These questions are all just more specific aliases for the general rule of thumb: does your application maintain data in-memory for longer than a single request?
The easiest and most clean approach to statelessness would be to ensure that you never instantiate or refer to anything outside the scope of a controller handler method. This means global.asax.cs is off limits, as are statics and singletons. The Application dictionary should also never be accessed. Code as follows should be considered a stateless fail:
// NEVER DO THIS IN THE CLOUD
Application["someData"] = toBeCached;// AND NEVER DO THIS, EITHER. EVER. EVER EVER.
someValue = Application["someData"];
Unfortunately, not all applications have the luxury of being able to support wholesale housecleaning of globals and singletons. In cases where legacy code prevents this, you’ll have to go through every single area where your controllers refer to globally-scoped data and refactor it class-by-class.
Caches and Reference Data
Imagining two instances of your application running in two different data centers and then envisioning how it will behave is an excellent way to get a rough idea for how stateless your service truly is.
However, after you’ve dealt with session state and obvious anti-stateless culprits like statics and singletons, we have some more subtle violations often pretending to be beneficial actors — wolves in sheep’s clothing, or some other clever analogy.
Let’s say your application has a cache stored in memory. “But it’s just to speed up access to reference data,” you say innocently. “It doesn’t really violate the stateless rule.”
The problem is, in-memory caches do violate the stateless rule, and they do so often in subtle ways that are extremely hard to detect in production. Let’s say you’ve got a cache of reference data and you’ve got 3 instances of your service running. Each maintains this cache to speed up access to this reference data.
Now let’s take a look at a timeline:
- Instance 1 receives a request
- Instance 1 has a cache miss, so it makes a query, updates the cache, and replies accordingly.
- The reference data in the database changes.
- Instance 2 receives a request, has a cache miss, so it makes a query and replies accordingly.
- Instance 3 receives a request, but it has a cache hit and replies immediately.
If we were to look at the responses returned from instances 1, 2, and 3, we will see that all three returned completely different data. Instance 1 returned data prior to the reference data change. Instance 2 returned the updated reference data, and instance 3 returned data of some unknown level of staleness because it may have been up and running for 3 weeks without its cache being invalidated.
Sure, there are common tricks people use to mitigate these scenarios like using notifications to force cache invalidation, but you see the core of the problem — in-memory caches are stateful.
If you have a cache of anything whether it’s reference data or seemingly innocuous information, if that cache is accessed to determine a service response, it must be refactored to an out-of-process cache.
There is a nearly infinite supply of cache products to choose from and whatever PaaS on which you’re running probably has a handful of options available in its marketplace. There’s a reason for this, and if you really want your application to thrive in the cloud, you’ll leave the caching code to the pros so you can focus solely on your service’s problem domain.