Check out my new blog at https://shibumiware.blogspot.com

Thursday, March 27, 2008

Monitor ASP.NET State Implemented in Microsoft SQL Server 2005

I realize this is a bit of a detour from the Continuous Integration series I was working on, but a priority work item popped regarding state management in ASP.NET.  I experimented with using Microsoft SQL Server 2005 to manage ASP.NET state today.   Click here for a very brief overview (backed by some great references) on how to quickly set this up.

After implementing state management in MSSQL2005, the first thing I did was setup a web site to put some state back into the server.  I ran a simple query on the state tables and discovered that state is tucked away in binary form--as it should be.   Always curious, I decided to write a little tool that would unpack this binary information and make it available for analysis.  Here were my original thoughts on the tool:

  1. The tool should monitor state and report it back such that state history is maintained, sort of like a "watch with memory".   This would allow the monitoring of state change over time.

  2. The first cut of the tool should support state monitoring of primitives only (int, string, bool, etc) but a subsequent cut might add support for monitoring object state.  This would require reflection and hooking the CurrentDomain_AssemblyResolve to figure out how to load the appropriate type.  I have done this before when writing an add-in for Microsoft Project.

  3. The tool should monitor for new session items, changed session items, and deleted session items, and tomorrow I will add support for monitoring session creation and deletion.

  4. The tool should should use asynchronous ADO.NET (just because I wanted to try this for the first time)

Let's start with the end result, a very simple session monitor WinForms application that is monitoring state for two separate ASP.NET web site:

image

The two browser instances allow the setting (and removing for two of the session items) of session item data.  The lower-most window is the state monitoring application.  Updates to the session are reflect in the grid. 

There are some issues with my current implementation but I only spent about an hour and a half on it, so stay with me on this.

You can see from the class diagram to the left what the Watch class looks like.   Because I am using SqlConnection and SqlCommand, I have implemented IDisposable.  The key methods are Open(), ReadSessionData(), GetSessionItemCollections(), and SynchSession().

Let's start with Open().  Note that all fields (_<field name>) are initialized in the constructor and include a server, database (ASP.NET session database), and a polling interval).

Implementation is below...

public void Open()
{
    Close();
 
    string connection = "data source={0}; trusted_connection=true;Asynchronous Processing=true;initial catalog={1}";
 
    _sqlConnection = new SqlConnection(string.Format(connection, _server, _database));
 
    _sqlConnection.Open();
 
    _sqlCommand =
        new SqlCommand("SELECT Locked, SessionID, SessionItemShort, SessionItemLong FROM ASPStateTempSessions",
                       _sqlConnection);
 
    _sqlCommand.BeginExecuteReader(ReadSessionData, _sqlCommand);
}

This method kicks off the asynchronous ADO.NET call BeginExecuteReader, which queries for the Locked, SessionID, SessionItemShort, and SessionItemLong attributes on each record in the ASPStateTempSessions table.  By the way, each session state record has an expiration date (derived, not surprisingly, from the session time-out) and is periodically purged from the table.

The callback provided to BeginExecuteReader is ReadSessionData, which is the most involved method in the class.  Keep in mind that I wanted to monitor state changes over time (which is imperfect here due to the polling interval, which will likely miss state changes if it is too long).  This is okay for my purposes, which are entirely exploratory.

A key data structure is the _sessions data structure:

private Dictionary<string, Dictionary<string, ArrayList>> _sessions = new Dictionary<string, Dictionary<string, ArrayList>>();

By the way, I just discovered a nice plugin for LiveWriter that allows posting of code in a readable manner.

The _sessions data structure is a Dictionary of session ID's and Dictionaries of a session Item key and its values over time. 

The body of ReadSessionData is below.  I read the data query result and call GetSessionItemCollections, which deserializes the binary data in SessionItemShort and SessionItemLong.

private void ReadSessionData(IAsyncResult result)
{
    _isRunning = true;
 
    using (SqlCommand asynchCommand = result.AsyncState as SqlCommand)
    {
        if (asynchCommand != null)
        {
            using (SqlDataReader reader = _sqlCommand.EndExecuteReader(result))
            {
                while (reader.Read())
                {
                    bool locked = (bool) reader["Locked"];
                    if (locked)
                        continue;
                    string sessionId = (string) reader["SessionID"];
 
                    if (!_sessions.ContainsKey(sessionId))
                    {
                        _sessions.Add(sessionId, new Dictionary<string, ArrayList>());
                    }
 
                    SessionStateItemCollection sessionItems;
                    HttpStaticObjectsCollection staticObjects;
 
                    if (reader["SessionItemShort"] != DBNull.Value)
                    {
                        GetSessionItemCollections((byte[]) reader["SessionItemShort"], out sessionItems, out staticObjects);
 
                        SynchCollections(sessionId, sessionItems);
                    }
 
                    if (reader["SessionItemLong"] != DBNull.Value)
                    {
                        GetSessionItemCollections((byte[]) reader["SessionItemLong"], out sessionItems, out staticObjects);
                        SynchCollections(sessionId, sessionItems);
                    }
                }
 
                reader.Close();
 
                if (_stop)
                {
                    _stoppedEvent.Set();
                }
                else
                {
                    Thread.Sleep(_pollInterval);
 
                    _sqlCommand.BeginExecuteReader(ReadSessionData, _sqlCommand);
                }
            }
        }
        _isRunning = false;
    }
}

In GetSessionItemCollections the bytes from the memory stream as read, pushing the offset through the data to collect specific fields.  Towards the end we use the SessionStateItemCollection and HttpStaticObjectsCollection classes to deserialize the session items (these classes are implemented in System.Web.SessionState).

I then call SynchCollections which is responsible for managing the _sessions data structure and raising events back to the client application.  

private void SynchCollections(string sessionId, SessionStateItemCollection sessionItems)
{            
    if (sessionItems != null)
    {
        SynchUpdateCollection(sessionItems, sessionId);
        SynchDeleteCollection(sessionItems, sessionId);
    }
    else
    {
        SynchClearCollection(sessionId);
    }
}

Three methods are called, depending on the incoming SessionStateItemCollection:  SynchUpdateCollection, SynchDeleteCollection, and SynchClearCollection:

SynchUpdateCollection motors through the session items and either creates a new item if it isn't present or attaches the latest value of the session item, if it has changed, to the key/value collection:

private void SynchUpdateCollection(SessionStateItemCollection sessionItems, string sessionId)
{
    foreach (string key in sessionItems.Keys)
    {
        // New name/value pair
        if (!_sessions[sessionId].ContainsKey(key))
        {
            _sessions[sessionId].Add(key, new ArrayList());
        }
 
        // Handle primitives + strings only (Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, Char, Double, and Single.)
        Type type = sessionItems[key].GetType();
        if (type.IsPrimitive || type == typeof (string))
        {
            bool add = false;
 
            if (_sessions[sessionId][key].Count > 0) // See if the name/value pair exists
            {
                // Yes, check the last value to see if it matches the current value
                if (Convert.ToString(_sessions[sessionId][key][_sessions[sessionId][key].Count - 1]) != Convert.ToString(sessionItems[key]))
                {
                    //  Nope, item has changed.
                    OnItemUpdated(this, key, sessionItems[key]);
                    add = true;
                }
            }
            else
            {
                //  The name value/pair doesn't exist, add it 
                OnNewItem(this, key, sessionItems[key]);
                add = true;
            }
 
            if (add)
            {
                _sessions[sessionId][key].Add(sessionItems[key]);
            }
        }
        else
        {
            // Handle non-primitive types
        }
    }
}

SynchDeleteCollection removes any session items from the local _sessions data structure if they key/value pair has been removed from the session state:

private void SynchDeleteCollection(SessionStateItemCollection sessionItems, string sessionId)
{
    List<string> _deletedKeys = new List<string>();
    Dictionary<string, ArrayList>.KeyCollection currentKeys = _sessions[sessionId].Keys;
    bool delete = true;
 
    foreach (string key in currentKeys)
    {
        foreach (string sessionKey in sessionItems.Keys)
        {
            if (sessionKey == key)
            {
                delete = false;
                break;
            }
        }
 
        if (delete)
            _deletedKeys.Add(key);
 
        delete = true;
    }
 
    foreach (string s in _deletedKeys)
    {
        _sessions[sessionId].Remove(s);
        OnItemRemoved(this, s);
    }
}

Finally, SynchClearCollection is called if SessionItemsCollection is empty:

private void SynchClearCollection(string sessionId)
{
    Dictionary<string, ArrayList>.KeyCollection currentKeys = _sessions[sessionId].Keys;
 
    if (currentKeys.Count == 0) return;
 
    foreach (string key in currentKeys)
    {
        OnItemRemoved(this, key);
    }
    _sessions[sessionId].Clear();
}

That's it for now... back to work!

No comments :

Disclaimer

Content on this site is provided "AS IS" with no warranties and confers no rights. Additionally, all content on this site is my own personal opinion and does not represent my employer's view in any way.