File: State\InProcStateClientManager.cs
Project: ndp\fx\src\xsp\system\Web\System.Web.csproj (System.Web)
//------------------------------------------------------------------------------
// <copyright file="InProcStateClientManager.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>                                                                
//------------------------------------------------------------------------------
 
namespace System.Web.SessionState {
    using System.Threading;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Runtime.Serialization;
 
    using System.Text;
    using System.Collections;
    using System.IO;
    using System.Web;
    using System.Web.Caching;
    using System.Web.Util;
    using System.Xml;
    using System.Collections.Specialized; 
    using System.Configuration.Provider;
 
    internal sealed class InProcSessionStateStore : SessionStateStoreProviderBase {
        internal static readonly int    CACHEKEYPREFIXLENGTH = CacheInternal.PrefixInProcSessionState.Length;
        internal static readonly int    NewLockCookie = 1;
 
        CacheItemRemovedCallback        _callback;
        
        SessionStateItemExpireCallback  _expireCallback;
 
 
        /*
         * Handle callbacks from the cache for session state expiry
         */
        public void OnCacheItemRemoved(String key, Object value, CacheItemRemovedReason reason) {
            InProcSessionState state;
            String id;
 
            Debug.Trace("SessionOnEnd", "OnCacheItemRemoved called, reason = " + reason);
 
            PerfCounters.DecrementCounter(AppPerfCounter.SESSIONS_ACTIVE);
 
            state = (InProcSessionState) value;
            
            if ((state._flags & (int)SessionStateItemFlags.IgnoreCacheItemRemoved) != 0 ||
                (state._flags & (int)SessionStateItemFlags.Uninitialized) != 0) {
                Debug.Trace("SessionOnEnd", "OnCacheItemRemoved ignored");
                return;
            }
            
            switch (reason) {
                case CacheItemRemovedReason.Expired: 
                    PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_TIMED_OUT);
                    break;
 
                case CacheItemRemovedReason.Removed:
                    PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_ABANDONED);
                    break;
 
                default:
                    break;    
            }
 
            TraceSessionStats();
 
            if (_expireCallback != null) {
                id = key.Substring(CACHEKEYPREFIXLENGTH);
 
                _expireCallback(id, SessionStateUtility.CreateLegitStoreData(null,
                                                        state._sessionItems,
                                                        state._staticObjects,
                                                        state._timeout));
            }
        }
 
        private string CreateSessionStateCacheKey(String id) {
            return CacheInternal.PrefixInProcSessionState + id;
        }
 
        public override void Initialize(string name, NameValueCollection config)
        {
            if (String.IsNullOrEmpty(name))
                name = "InProc Session State Provider";
            base.Initialize(name, config);
 
            _callback = new CacheItemRemovedCallback(this.OnCacheItemRemoved);
        }
 
        public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
        {
            _expireCallback = expireCallback;
            return true;
        }
        
        public override void Dispose()
        {
        }
 
        public override void InitializeRequest(HttpContext context)
        {
        }
 
        SessionStateStoreData DoGet(HttpContext context, 
                                        String id,
                                        bool exclusive,
                                        out bool locked,
                                        out TimeSpan lockAge, 
                                        out object lockId,
                                        out SessionStateActions actionFlags) {
            string  key = CreateSessionStateCacheKey(id);
 
            // Set default return values
            locked = false;
            lockId = null;
            lockAge = TimeSpan.Zero;
            actionFlags = 0;
 
            // Not technically necessary for InProc, but we do it to be consistent
            // with SQL provider
            SessionIDManager.CheckIdLength(id, true /* throwOnFail */);
 
            InProcSessionState state = (InProcSessionState)HttpRuntime.Cache.InternalCache.Get(key);
            if (state != null) {
                bool    lockedByOther;       // True if the state is locked by another session
                int initialFlags;
 
                initialFlags = (int)state._flags;
                if ((initialFlags & (int)SessionStateItemFlags.Uninitialized) != 0) {
                    // It is an uninitialized item.  We have to remove that flag.
                    // We only allow one request to do that.
                    // For details, see inline doc for SessionStateItemFlags.Uninitialized flag.
 
                    // If initialFlags != return value of CompareExchange, it means another request has
                    // removed the flag.
 
                    Debug.Trace("SessionStateClientSet", "Removing the Uninit flag for item; key = " + key);
                    if (initialFlags == Interlocked.CompareExchange(
                                            ref state._flags, 
                                            initialFlags & (~((int)SessionStateItemFlags.Uninitialized)), 
                                            initialFlags)) {
                        actionFlags = SessionStateActions.InitializeItem;
                    }
                }
 
                if (exclusive) {
                    lockedByOther = true;
                    
                    // If unlocked, use a spinlock to test and lock the state.
                    if (!state._locked) {
                        state._spinLock.AcquireWriterLock();
                        try {
                            if (!state._locked) {
                                lockedByOther = false;
                                state._locked = true;
                                state._utcLockDate = DateTime.UtcNow;
                                state._lockCookie++;
                            }
                            lockId = state._lockCookie;
                        }
                        finally {
                            state._spinLock.ReleaseWriterLock();
                        }
                    }
                    else {
                        // It's already locked by another request.  Return the lockCookie to caller.
                        lockId = state._lockCookie;
                    }
 
                }
                else {
                    state._spinLock.AcquireReaderLock();
                    try {
                        lockedByOther = state._locked;
                        lockId = state._lockCookie;
                    }
                    finally {
                        state._spinLock.ReleaseReaderLock();
                    }
                }
                
                if (lockedByOther) {
                    // Item found, but locked
                    locked = true;
                    lockAge = DateTime.UtcNow - state._utcLockDate;
                    return null;
                }
                else {
                    return SessionStateUtility.CreateLegitStoreData(context, state._sessionItems,
                                                state._staticObjects, state._timeout);
                }
            }
 
            // Not found
            return null;
        }
 
        public override SessionStateStoreData GetItem(HttpContext context, 
                                                String id,
                                                out bool locked,
                                                out TimeSpan lockAge, 
                                                out object lockId,
                                                out SessionStateActions actionFlags) {
            return DoGet(context, id, false, out locked, out lockAge, out lockId, out actionFlags);
        }
 
 
        public override SessionStateStoreData GetItemExclusive(HttpContext context, 
                                                String id,
                                                out bool locked,
                                                out TimeSpan lockAge, 
                                                out object lockId,
                                                out SessionStateActions actionFlags) {
            return DoGet(context, id, true, out locked, out lockAge, out lockId, out actionFlags);
        }
 
        // Unlock an item locked by GetExclusive
        // 'lockId' is the lock context returned by previous call to GetExclusive
        public override void ReleaseItemExclusive(HttpContext context, 
                                String id, 
                                object lockId) {
            Debug.Assert(lockId != null, "lockId != null");
            
            string  key = CreateSessionStateCacheKey(id);
            int     lockCookie = (int)lockId;
            
            SessionIDManager.CheckIdLength(id, true /* throwOnFail */);
 
            InProcSessionState state = (InProcSessionState)HttpRuntime.Cache.InternalCache.Get(key);
 
            /* If the state isn't there, we probably took too long to run. */
            if (state == null)
                return;
 
            if (state._locked) {
                state._spinLock.AcquireWriterLock();
                try {
                    if (state._locked && lockCookie == state._lockCookie) {
                        state._locked = false;
                    }
                }
                finally {
                    state._spinLock.ReleaseWriterLock();
                }
            }
        }
 
        public override void SetAndReleaseItemExclusive(HttpContext context, 
                                    String id, 
                                    SessionStateStoreData item, 
                                    object lockId, 
                                    bool newItem) {
            string          key = CreateSessionStateCacheKey(id);
            bool            doInsert = true;
            CacheStoreProvider     cacheInternal = HttpRuntime.Cache.InternalCache;
            int             lockCookieForInsert = NewLockCookie;
            ISessionStateItemCollection items = null;
            HttpStaticObjectsCollection staticObjects = null;
 
            Debug.Assert(item.Items != null, "item.Items != null");
            Debug.Assert(item.StaticObjects != null, "item.StaticObjects != null");
            Debug.Assert(item.Timeout <= SessionStateModule.MAX_CACHE_BASED_TIMEOUT_MINUTES, "item.Timeout <= SessionStateModule.MAX_CACHE_BASED_TIMEOUT_MINUTES");
 
            SessionIDManager.CheckIdLength(id, true /* throwOnFail */);
 
            if (item.Items.Count > 0) {
                items = item.Items;
            }
 
            if (!item.StaticObjects.NeverAccessed) {
                staticObjects = item.StaticObjects;
            }
 
            if (!newItem) {
                Debug.Assert(lockId != null, "lockId != null");
                InProcSessionState  stateCurrent = (InProcSessionState) cacheInternal.Get(key);
                int                 lockCookie = (int)lockId;
 
                /* If the state isn't there, we probably took too long to run. */
                if (stateCurrent == null)
                    return;
 
                Debug.Trace("SessionStateClientSet", "state is inStorage; key = " + key);
                Debug.Assert((stateCurrent._flags & (int)SessionStateItemFlags.Uninitialized) == 0, "Should never set an unitialized item; key = " + key);
                
                stateCurrent._spinLock.AcquireWriterLock();
                
                try {
                    /* Only set the state if we are the owner */
                    if (!stateCurrent._locked || stateCurrent._lockCookie != lockCookie) {
                        Debug.Trace("SessionStateClientSet", "Leave because we're not the owner; key = " + key);
                        return;
                    }
 
                    /* We can change the state in place if the timeout hasn't changed */
                    if (stateCurrent._timeout == item.Timeout) {
                        stateCurrent.Copy(
                            items,
                            staticObjects,
                            item.Timeout,
                            false,
                            DateTime.MinValue,
                            lockCookie,
                            stateCurrent._flags);
 
                        // Don't need to insert into the Cache because an in-place copy is good enough.
                        doInsert = false;
                        Debug.Trace("SessionStateClientSet", "Changing state inplace; key = " + key);
                    }
                    else {
                        /* We are going to insert a new item to replace the current one in Cache
                           because the expiry time has changed.
                           
                           Pleas note that an insert will cause the Session_End to be incorrectly raised. 
                           
                           Please note that the item itself should not expire between now and
                           where we do UtcInsert below because cacheInternal.Get above have just
                           updated its expiry time.
                        */ 
                        stateCurrent._flags |= (int)SessionStateItemFlags.IgnoreCacheItemRemoved;
                        
                        /* By setting _lockCookie to 0, we prevent an overwriting by ReleaseExclusive 
                           when we drop the lock.
                           The scenario can happen if another request is polling and trying to prempt
                           the lock we have on the item.
                        */
                        lockCookieForInsert = lockCookie;
                        stateCurrent._lockCookie = 0;
                    }
                }
                finally {
                    stateCurrent._spinLock.ReleaseWriterLock();
                }
            } 
 
            if (doInsert) {
                Debug.Trace("SessionStateClientSet", "Inserting state into Cache; key = " + key);
                InProcSessionState state = new InProcSessionState(
                        items,
                        staticObjects,
                        item.Timeout,
                        false,
                        DateTime.MinValue,
                        lockCookieForInsert,
                        0);
 
                try {
                }
                finally {
                    // protected from ThreadAbortEx
                    cacheInternal.Insert(key, state, new CacheInsertOptions() {
                                                            SlidingExpiration = new TimeSpan(0, state._timeout, 0),
                                                            Priority = CacheItemPriority.NotRemovable,
                                                            OnRemovedCallback = _callback
                                                        });
                    PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_TOTAL);
                    PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_ACTIVE);
 
                    TraceSessionStats();
                }
            }
        }
 
        
        public override void CreateUninitializedItem(HttpContext context, String id, int timeout) {
            string          key = CreateSessionStateCacheKey(id);
 
            Debug.Assert(timeout <= SessionStateModule.MAX_CACHE_BASED_TIMEOUT_MINUTES, "item.Timeout <= SessionStateModule.MAX_CACHE_BASED_TIMEOUT_MINUTES");
            
            SessionIDManager.CheckIdLength(id, true /* throwOnFail */);
 
            Debug.Trace("SessionStateClientSet", "Inserting an uninitialized item into Cache; key = " + key);
 
            InProcSessionState state = new InProcSessionState(
                    null,
                    null,
                    timeout,
                    false,
                    DateTime.MinValue,
                    NewLockCookie,
                    (int)SessionStateItemFlags.Uninitialized);
 
            // DevDivBugs 146875
            // We do not want to overwrite an item with an uninitialized item if it is
            // already in the cache
            try {
            }
            finally {
                // protected from ThreadAbortEx
                object existingEntry = HttpRuntime.Cache.InternalCache.Add(key, state, new CacheInsertOptions() {
                                                                                        SlidingExpiration = new TimeSpan(0, timeout, 0),
                                                                                        Priority = CacheItemPriority.NotRemovable,
                                                                                        OnRemovedCallback = _callback
                                                                                    });
                if (existingEntry == null) {
                    PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_TOTAL);
                    PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_ACTIVE);
                }
            }
        }
        
        // Remove an item.  Note that the item is originally obtained by GetExclusive
        // Same note as Set on lockId
        public override void RemoveItem(HttpContext context, 
                                        String id, 
                                        object lockId, 
                                        SessionStateStoreData item) {
            Debug.Assert(lockId != null, "lockId != null");
                
            string          key = CreateSessionStateCacheKey(id);
            CacheStoreProvider     cacheInternal = HttpRuntime.Cache.InternalCache;
            int             lockCookie = (int)lockId;
 
            SessionIDManager.CheckIdLength(id, true /* throwOnFail */);
 
            InProcSessionState state = (InProcSessionState) cacheInternal.Get(key);
 
            /* If the item isn't there, we probably took too long to run. */
            if (state == null)
                return;
 
            state._spinLock.AcquireWriterLock();
            
            try {
                /* Only remove the item if we are the owner */
                if (!state._locked || state._lockCookie != lockCookie)
                    return;
 
                /* prevent overwriting when we drop the lock */
                state._lockCookie = 0;
            }
            finally {
                state._spinLock.ReleaseWriterLock();
            }
 
            cacheInternal.Remove(key);
 
            TraceSessionStats();
        }
 
        // Reset the expire time of an item based on its timeout value
        public override void ResetItemTimeout(HttpContext context, String id)
        {
            string  key = CreateSessionStateCacheKey(id);
            
            SessionIDManager.CheckIdLength(id, true /* throwOnFail */);
            HttpRuntime.Cache.InternalCache.Get(key);
        }
 
        // Create a new SessionStateStoreData.
        public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
        {
            return SessionStateUtility.CreateLegitStoreData(context, null, null, timeout);
        }
 
        // Called during EndRequest event
        public override void EndRequest(HttpContext context)
        {
        }
        
        [System.Diagnostics.Conditional("DBG")]
        internal static void TraceSessionStats() {
#if DBG
            Debug.Trace("SessionState", 
                        "sessionsTotal="          + PerfCounters.GetCounter(AppPerfCounter.SESSIONS_TOTAL) + 
                        ", sessionsActive="       + PerfCounters.GetCounter(AppPerfCounter.SESSIONS_ACTIVE) + 
                        ", sessionsAbandoned="    + PerfCounters.GetCounter(AppPerfCounter.SESSIONS_ABANDONED) + 
                        ", sessionsTimedout="     + PerfCounters.GetCounter(AppPerfCounter.SESSIONS_TIMED_OUT)
                        );
#endif
        }
    }
 
    internal sealed class InProcSessionState {
        internal ISessionStateItemCollection         _sessionItems;
        internal HttpStaticObjectsCollection    _staticObjects;
        internal int                            _timeout;        // USed to set slidingExpiration in CacheEntry
        internal bool                           _locked;         // If it's locked by another thread
        internal DateTime                       _utcLockDate;
        internal int                            _lockCookie;
        #pragma warning disable 0649
        internal ReadWriteSpinLock              _spinLock;
        #pragma warning restore 0649
        internal int                            _flags;
 
        internal InProcSessionState(
                ISessionStateItemCollection      sessionItems, 
                HttpStaticObjectsCollection staticObjects, 
                int                         timeout,
                bool                        locked,
                DateTime                    utcLockDate,
                int                         lockCookie,
                int                         flags) {
 
            Copy(sessionItems, staticObjects, timeout, locked, utcLockDate, lockCookie, flags);
        }
 
        internal void Copy(
                ISessionStateItemCollection      sessionItems, 
                HttpStaticObjectsCollection staticObjects, 
                int                         timeout,
                bool                        locked,
                DateTime                    utcLockDate,
                int                         lockCookie,
                int                         flags) {
 
            this._sessionItems = sessionItems;
            this._staticObjects = staticObjects;
            this._timeout = timeout;
            this._locked = locked;
            this._utcLockDate = utcLockDate;
            this._lockCookie = lockCookie;
            this._flags = flags;
        }
    }
}