Building a Sitecore Redis Session State Provider

With the release of Sitecore 7.5, Sitecore changed their underlying architecture and introduced MongoDB. While this improves the performance of Experience Analytics (formerly Sitecore Analytics), it has brought with it a new set of challenges.

The new architecture persists all user interactions into Session until the session ends at which point it is pushed to MongoDB. Unfortunately many session state providers do not support the Session_End() event as it is not mandatory.

Sitecore's workaround is to provide their own session state providers for both MongoDB and SQL. These providers use a timer to periodically check for and end expired sessions. At the time of writing, Sitecore does not currently have a Redis compatible session state provider and while there is an officially supported Microsoft provider it does not support the Session_End() event.

Nick Hills has covered the basics of creating your own Sitecore Redis SessionState provider, although there is some room to expand especially with Redis Keyspace Notifications becoming more popular.

The high level solution is as follows:

  • Create a basic provider that proxies calls to an instance of the Microsoft Redis Session State Provider.
  • Enhance the session read/write methods so each request that the Microsoft provider performs also creates/updates an additional timeout key following the same format ApplicationName_SessionId_Timeout. The TTL for this key should be approximately one minute less than the timeout of the _Data and _Internal keys.
  • Subscribe to Redis Keyspace Notifications matching the pattern __keyspace@0__:{ApplicationName}_*_Timeout using ServiceStack.Redis.
  • Create an expiry notification handler to trigger the Session_End() for each expired _Timeout key.

Creating a basic provider

Create a basic session provider class that inherits from SessionStateStoreProviderBase. Is it important to note that the provider must not be static. Under load a static provider will have intermittent failures.


public class RedisSessionStateProvider : SessionStateStoreProviderBase
{
    private Microsoft.Web.Redis.RedisSessionStateProvider _redisSessionStateProviderContext;
    private SessionStateItemExpireCallback _sessionEndCallback;
    private string _applicationName;
    private TimeSpan _timeout;

    public RedisSessionStateProvider()
    {
        _redisSessionStateProviderContext = new Microsoft.Web.Redis.RedisSessionStateProvider();
    }
}

Create an override for the Initialise() and SetItemExpireCallback() methods. The first to create instances of both the Microsoft and ServiceStack providers. The second to ensure .NET is notified that the provider supports Session_End().


public override void Initialize(string name, NameValueCollection config)
{
    _redisSessionStateProviderContext.Initialize(name, config);

    _connectionMultiplexer = ConnectionMultiplexer.Connect(new ConfigurationOptions
    {
        _applicationName = config["applicationName"];
    });

    var sessionStateSection = (SessionStateSection)WebConfigurationManager.GetSection("system.web/sessionState");
    _timeout = sessionStateSection.Timeout;
}

public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
{
    _sessionEndCallback = expireCallback;
    return null != _sessionEndCallback;
}

Create overrides for most of the methods on the base provider and proxy calls through to the current Microsoft provider _redisSessionStateProviderContext. Do not populate the Dispose() method - Sitecore will call it throughout the application life cycle causing intermittent issues. For brevity only a few full examples are shown below:


public override void InitializeRequest(HttpContext context)
{
    _redisSessionStateProviderContext.InitializeRequest(context);
}

public override SessionStateStoreData GetItem(HttpContext context, string id, ... )
{
    return _redisSessionStateProviderContext.GetItem(context, id, ... );
}

public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, ... )
{
    return _redisSessionStateProviderContext.GetItemExclusive(context, id, ... );
}

public override void Dispose()
{
}

public override void ReleaseItemExclusive(HttpContext context, string id, ... )
...

public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
...

public override void EndRequest(HttpContext context)
...

public override void RemoveItem(HttpContext context, string id, ... )
...

public override void ResetItemTimeout(HttpContext context, string id)
...

public override void CreateUninitializedItem(HttpContext context, string id, int timeout)
...


Enhance the session read/write methods

Update the methods above so each request that the Microsoft provider performs, also creates/updates an additional timeout key following the same format ApplicationName_SessionId_Timeout. The TTL for this key should be approximately one minute less than the timeout of the _Data and _Internal keys. Once again for brevity only an example is shown below:


public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, ... )
{
    var db = _connectionMultiplexer.GetDatabase();
    var key = string.Format("{0}_{1}_Timeout", _applicationName, id);
    db.StringSet(key, id, _timeout - TimeSpan.FromMinutes(1));
    return _redisSessionStateProviderContext.GetItemExclusive(context, id, ... );
}

Create an expiry handler

Create a method to handle expired messages following the example below. The expiry handler should ensure that it only actions events named expired (not to be confused with expire) and triggers the Session_End() event.


private void Handle(RedisChannel channel, RedisValue value)
{
    if (value.HasValue && value.ToString().Contains("expired"))
    {
        // Get the session ID from channel
        // A string replace/split or RegEx will suffice
        // var id = channel.ToString().Split('_')[5];

        // Fire Session_End() event
        // _sessionEndCallback(id, session);
    }
}

Subscribe to Redis Keyspace Notifications

Now update the Initialize() method to subscribe to the correct Keyspace Notifications and call the Handle method for each notification received.


public override void Initialize(string name, NameValueCollection config)
{
    ...
    _subscriber = _connectionMultiplexer.GetSubscriber();
    _subscriber.Subscribe(string.Format("__keyspace@0__:{0}_*_Timeout", _applicationName), Handle);
    ...
}

Implement

The solution above is ready to integrate. Once integrated into the Web.config and the Sitecore.Analytics.Tracking.config data will begin to show in Redis and then eventually MongoDB (on Session_End()) as shown below:
Redis

Performance

The integrated solution performs almost exactly as the ServiceStack provider and has passed preliminary performance testing of ~15 concurrent users as shown below:

Redis Statistics

Notes

Please consider the following:

  • There is no guarantee that the Redis server will be able to generate the expired event at the time the key TTL reaches zero.

  • While the above example uses Keyspace Notifications, Keyevent Notifications could be used to reduce the amount of overhead traffic (one watches all keys by name and the other watches a specific key for all events).

  • At various points in the application's life-cycle, there may be one or more instances of the session state provider due to Sitecore creating/disposing them while performing certain actions.

  • Each instance of the session state provider will consume two connections to the Redis database. If the Microsoft provider exposed it's IConnectionMultiplexer the connection could potentially be re-used.

  • Additional configuration needs to be extracted manually from the NameValueCollection as Microsoft have made their classes private. At a minimum the provider should support timeout, connectionTimeoutInMilliseconds, operationTimeoutInMilliseconds, retryTimeoutInMilliseconds, ssl, host and port.

  • Both the Microsoft and ServiceStack providers assume db0 as the default database.