Turn off Session State locking in Sitecore MVC pages

The default implementation of the ASP .NET Session State Module uses exclusive locking for each request from the same session. This means ASP .NET will only execute one request at a time from the same browser. Any other request will be locked by the Session State Module and will not be executed until the previous request is complete and it can obtain the exclusive lock. This can cause performance issues in many real-world scenarios.

Below screenshot from IIS shows 6 concurrent request to the homepage from the same browser. Sitecore is only executing the bottom request, which is in the ExecuteRequestHandler state. All other 5 requests are in the RequestAcquireState state and will only be fulfilled one at a time after the bottom request is complete. Each of the requests in RequestAcquireState state will check the session store every 0.5 seconds to see if it can obtain a lock.

This can cause pressure on the session state store in case many requests take some time to execute. Depending on the session store it is common to see messages like below in log:

Common errors with session state in Redis:

Exception type: TimeoutException
Exception message: Timeout performing EVAL, inst: ....
at StackExchange.Redis.ConnectionMultiplexer.ExecuteSyncImpl[T](Message message, ResultProcessor1 processor, ServerEndPoint server) at StackExchange.Redis.RedisBase.ExecuteSync[T](Message message, ResultProcessor1 processor, ServerEndPoint server)
at StackExchange.Redis.RedisDatabase.ScriptEvaluate(String script, RedisKey[] keys, RedisValue[] values, CommandFlags flags)
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.<>c__DisplayClass7.b__6()
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryForScriptNotFound(Func1 redisOperation) at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.RetryLogic(Func1 redisOperation)
at Sitecore.SessionProvider.Redis.StackExchangeClientConnection.Eval(String script, String[] keyArgs, Object[] valueArgs)
at Sitecore.SessionProvider.Redis.RedisConnectionWrapper.TryTakeWriteLockAndGetData(String sessionId, DateTime lockTime, Object& lockId, ISessionStateItemCollection& data, Int32& sessionTimeout)
at Sitecore.SessionProvider.Redis.RedisSessionStateProvider.GetItemFromSessionStore(Boolean isWriteLockRequired, HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
at Sitecore.SessionProvider.Redis.RedisSessionStateProvider.GetItemExclusive(HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
at System.Web.SessionState.SessionStateModule.GetSessionStateItem()

Common errors with session state in SQL:

Message: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
Source: System.Data
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   ... 
   at System.Web.SessionState.SqlSessionStateStore.SqlStateConnection..ctor(SqlPartitionInfo sqlPartitionInfo, TimeSpan retryInterval)
Message: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
Source: System.Data
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   ... 
   at System.Web.SessionState.SqlSessionStateStore.SqlStateConnection..ctor(SqlPartitionInfo sqlPartitionInfo, TimeSpan retryInterval)

Common errors with session state in Mongo:

ERROR Application error.
Exception: System.TimeoutException
Message: Timeout waiting for a MongoConnection.
Source: MongoDB.Driver
   at MongoDB.Driver.Internal.MongoConnectionPool.AcquireConnection(AcquireConnectionOptions options)
   ...
   at Sitecore.SessionProvider.MongoDB.MongoSessionStateProvider.GetItemExclusive(HttpContext context, String id, Boolean& locked, TimeSpan& lockAge, Object& lockId, SessionStateActions& actions)
   at System.Web.SessionState.SessionStateModule.GetSessionStateItem()
   at System.Web.SessionState.SessionStateModule.BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData)
   at System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Too many locked requests from a single session

ERROR Application error.
Exception: System.Web.HttpException
Message: The request queue limit of the session is exceeded.
Source: System.Web
   at System.Web.SessionState.SessionStateModule.QueueRef()
   at System.Web.SessionState.SessionStateModule.PollLockedSession()
   at System.Web.SessionState.SessionStateModule.GetSessionStateItem()
   at System.Web.SessionState.SessionStateModule.BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData)
   at System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Sitecore has a good KB article which describes this in more detail which can be found here. This article mentions to set session state to readonly and describes how to do this for 2 scenarios:

  • Custom MVC Routes: Set the session state to readonly on the controller. This can be done by decorating the controller with this attribute: [SessionState(SessionStateBehavior.ReadOnly)]
  • ASP.NET Web Forms pages: Set the EnableSessionState=”Readonly” on the pages directive

This article does not mention how to fix this for Sitecore MVC pages. The solution provided below describes how to address this for Sitecore MVC pages.

Solution

Sitecore sets this to the Default Session state behavior in the SitecoreControllerFactory for Sitecore MVC pages. This is a virtual method so this can be overridden to change the session state behavior:

using Sitecore.Diagnostics;
using Sitecore.Mvc.Controllers;
using Sitecore.Mvc.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;

namespace Foundation.Extensions.Factory
{
    public class ReadOnlySessionStateSitecoreControllerFactory : SitecoreControllerFactory
    {
        public ReadOnlySessionStateSitecoreControllerFactory(IControllerFactory innerFactory) : base(innerFactory)
        {
        }

        public override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
        {
            Assert.ArgumentNotNull(requestContext, "requestContext");
            Assert.ArgumentNotNull(controllerName, "controllerName");

            if (controllerName.EqualsText(SitecoreControllerName))
            {
                return SessionStateBehavior.ReadOnly;
            }

            return InnerFactory.GetControllerSessionBehavior(requestContext, controllerName);
        }
    }
}

An initialize pipeline processor needs to be created to set our new controller factory:

using Foundation.SitecoreExtensions.Factory;
using Sitecore.Mvc.Controllers;
using Sitecore.Mvc.Pipelines.Loader;
using Sitecore.Pipelines;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Foundation.Extensions.Processors.Initialize
{
    public class InitializeReadOnlySessionStateSitecoreControllerFactory : InitializeControllerFactory
    {
        protected Func<System.Web.Mvc.ControllerBuilder> ControllerBuilder = () => System.Web.Mvc.ControllerBuilder.Current;

        protected override void SetControllerFactory(PipelineArgs args)
        {
            System.Web.Mvc.ControllerBuilder controllerBuilder = ControllerBuilder();
            var controllerFactory = new ReadOnlySessionStateSitecoreControllerFactory(controllerBuilder.GetControllerFactory());
            controllerBuilder.SetControllerFactory(controllerFactory);
        }
    }
}

Below XML file can be used to patch in this new pipeline processor

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="Foundation.Extensions.Processors.Initialize.InitializeReadOnlySessionStateSitecoreControllerFactory, Foundation.Extensions" patch:instead="*[@type='Sitecore.Mvc.Pipelines.Loader.InitializeControllerFactory, Sitecore.Mvc']"/>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Below screenshot shows the same scenario as in the beginning of this post, but now all 8 requests are getting executed at the same time.

Setting the session state to readonly for Sitecore MVC pages can cause significant performance improvements and will help reduce the load on the session store as described in Sitecore’s KB article. Before doing this it is important to understand below considerations:

  • Multiple requests from the same browser will execute at the same time. Your application should be able to handle this without causing any unintended issues by multiple threads modifying shared objects at the same time.
  • Custom objects cannot be stored in the session state anymore when it is set to ReadOnly, except when the session state is in process. Using a custom cache as already suggested in Sitecore’s article is a good solution.
  • This issue might not occur when a site is running smoothly, but can turn a small issue into an overall site stability issue. The session store can get under a lot of load for example if some pages in your site start being slow or in case of an app pool recycle. This can impact the overall stability of the site as it can overload SQL, Redis or Mongo.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s