blob: e23c52538cde8b2c4ebd42c29229a73dc9d3f4da [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Apache.Ignite.AspNet
{
using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Threading;
using System.Web;
using System.Web.SessionState;
using Apache.Ignite.AspNet.Impl;
using Apache.Ignite.Core;
using Apache.Ignite.Core.Binary;
using Apache.Ignite.Core.Cache;
using Apache.Ignite.Core.Common;
using Apache.Ignite.Core.Impl.Cache;
/// <summary>
/// ASP.NET Session-State Store Provider that uses Ignite distributed cache as an underlying storage.
/// <para />
/// You can either start Ignite yourself, and provide <c>gridName</c> attribute,
/// or provide <c>igniteConfigurationSectionName</c> attribute to start Ignite automatically from specified
/// configuration section (see <see cref="IgniteConfigurationSection"/>)
/// using <c>igniteConfigurationSectionName</c>.
/// <para />
/// <c>cacheName</c> attribute specifies Ignite cache name to use for data storage. This attribute can be omitted
/// if cache name is null.
/// <para />
/// Optional <c>applicationId</c> attribute allows sharing a single Ignite cache between multiple web applications.
/// </summary>
public class IgniteSessionStateStoreProvider : SessionStateStoreProviderBase
{
/// <summary>
/// The default cache name to be used when <c>cacheName</c> attribute is not specified.
/// </summary>
public const string DefaultCacheName = "ASPNET_SESSION_STATE";
/** Extension id */
private const int ExtensionId = 0;
/// <summary>
/// Op codes for <see cref="ICacheInternal.DoOutInOpExtension{T}"/>.
/// </summary>
private enum Op
{
/** Lock the session data. */
Lock = 1,
/** Update and unlock the session data. */
SetAndUnlock = 2,
/** Get the data without lock. */
Get = 3,
/** Put the data without lock. */
Put = 4,
/** Remove the data without lock. */
Remove = 5
}
/** Application id config parameter. */
private const string ApplicationId = "applicationId";
/** */
private volatile string _applicationId;
/** */
private volatile ExpiryCacheHolder<string, IgniteSessionStateStoreData> _expiryCacheHolder;
/** */
private static long _lockId;
/// <summary>
/// Initializes the provider.
/// </summary>
/// <param name="name">The friendly name of the provider.</param>
/// <param name="config">A collection of the name/value pairs representing the provider-specific attributes
/// specified in the configuration for this provider.</param>
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
public override void Initialize(string name, NameValueCollection config)
{
base.Initialize(name, config);
var cache = ConfigUtil.InitializeCache<string, IgniteSessionStateStoreData>(config, GetType(),
DefaultCacheName);
_expiryCacheHolder = new ExpiryCacheHolder<string, IgniteSessionStateStoreData>(cache);
_applicationId = config[ApplicationId];
}
/// <summary>
/// Releases all resources used by the <see cref="System.Web.SessionState.SessionStateStoreProviderBase" />
/// implementation.
/// </summary>
public override void Dispose()
{
// No-op.
}
/// <summary>
/// Sets a reference to the <see cref="System.Web.SessionState.SessionStateItemExpireCallback" />
/// delegate for the Session_OnEnd event defined in the Global.asax file.
/// </summary>
/// <param name="expireCallback">The <see cref="System.Web.SessionState.SessionStateItemExpireCallback" />
/// delegate for the Session_OnEnd event defined in the Global.asax file.</param>
/// <returns>
/// true if the session-state store provider supports calling the Session_OnEnd event; otherwise, false.
/// </returns>
public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
{
// Expiration events not supported for now.
return false;
}
/// <summary>
/// Called by the <see cref="System.Web.SessionState.SessionStateModule" /> object
/// for per-request initialization.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
public override void InitializeRequest(HttpContext context)
{
// No-op.
}
/// <summary>
/// Returns read-only session-state data from the session data store.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
/// <param name="id">The <see cref="System.Web.SessionState.HttpSessionState.SessionID" /> for the
/// current request.</param>
/// <param name="locked">When this method returns, contains a Boolean value that is set to true if the
/// requested session item is locked at the session data store; otherwise, false.</param>
/// <param name="lockAge">When this method returns, contains a <see cref="System.TimeSpan" /> object that
/// is set to the amount of time that an item in the session data store has been locked.</param>
/// <param name="lockId">When this method returns, contains an object that is set to the lock identifier
/// for the current request. For details on the lock identifier, see "Locking Session-Store Data"
/// in the <see cref="System.Web.SessionState.SessionStateStoreProviderBase" /> class summary.</param>
/// <param name="actions">When this method returns, contains one of the
/// <see cref="System.Web.SessionState.SessionStateActions" /> values, indicating whether the current
/// session is an uninitialized, cookieless session.</param>
/// <returns>
/// A <see cref="System.Web.SessionState.SessionStateStoreData" /> populated with session values and
/// information from the session data store.
/// </returns>
public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked,
out TimeSpan lockAge, out object lockId,
out SessionStateActions actions)
{
actions = SessionStateActions.None;
lockId = null;
lockAge = TimeSpan.Zero;
locked = false;
var key = GetKey(id);
var data = GetItem(key);
if (data != null)
{
locked = data.LockNodeId != null;
if (!locked)
{
return data;
}
Debug.Assert(data.LockTime != null);
lockAge = DateTime.UtcNow - data.LockTime.Value;
lockId = data.LockId;
return null;
}
return null;
}
/// <summary>
/// Returns read-only session-state data from the session data store.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
/// <param name="id">The <see cref="System.Web.SessionState.HttpSessionState.SessionID" /> for the current
/// request.</param>
/// <param name="locked">When this method returns, contains a Boolean value that is set to true if a lock
/// is successfully obtained; otherwise, false.</param>
/// <param name="lockAge">When this method returns, contains a <see cref="System.TimeSpan" /> object that
/// is set to the amount of time that an item in the session data store has been locked.</param>
/// <param name="lockId">When this method returns, contains an object that is set to the lock identifier
/// for the current request. For details on the lock identifier, see "Locking Session-Store Data" in
/// the <see cref="System.Web.SessionState.SessionStateStoreProviderBase" /> class summary.</param>
/// <param name="actions">When this method returns, contains one of the
/// <see cref="System.Web.SessionState.SessionStateActions" /> values, indicating whether the current
/// session is an uninitialized, cookieless session.</param>
/// <returns>
/// A <see cref="System.Web.SessionState.SessionStateStoreData" /> populated with session values
/// and information from the session data store.
/// </returns>
public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked,
out TimeSpan lockAge,
out object lockId, out SessionStateActions actions)
{
actions = SessionStateActions.None; // Our items never need initialization.
lockAge = TimeSpan.Zero;
lockId = null;
var lockId0 = Interlocked.Increment(ref _lockId);
var key = GetKey(id);
var lockResult = LockItem(key, lockId0);
// No item found.
if (lockResult == null)
{
locked = false;
return null;
}
// Item was already locked.
if (!lockResult.Success)
{
locked = true;
Debug.Assert(lockResult.LockTime != null);
lockAge = DateTime.UtcNow - lockResult.LockTime.Value;
lockId = lockResult.LockId;
return null;
}
// Item found and lock obtained.
locked = false;
lockId = lockId0;
if (lockId0 != lockResult.Data.LockId)
throw new IgniteException(string.Format(CultureInfo.InvariantCulture,
"Invalid session state lock result, " +
"expected lockId: {0}, actual: {1}", lockId0, lockResult.Data.LockId));
return lockResult.Data;
}
/// <summary>
/// Releases a lock on an item in the session data store.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
/// <param name="id">The session identifier for the current request.</param>
/// <param name="lockId">The lock identifier for the current request.</param>
public override void ReleaseItemExclusive(HttpContext context, string id, object lockId)
{
UnlockItem(GetKey(id), (long) lockId);
}
/// <summary>
/// Updates the session-item information in the session-state data store with values from the current request,
/// and clears the lock on the data.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
/// <param name="id">The session identifier for the current request.</param>
/// <param name="item">The <see cref="System.Web.SessionState.SessionStateStoreData" /> object that
/// contains the current session values to be stored.</param>
/// <param name="lockId">The lock identifier for the current request.</param>
/// <param name="newItem">true to identify the session item as a new item; false to identify the session
/// item as an existing item.</param>
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item,
object lockId, bool newItem)
{
Debug.Assert(item != null);
var key = GetKey(id);
var data = (IgniteSessionStateStoreData) item;
if (newItem)
{
var cache = _expiryCacheHolder.GetCacheWithExpiry(data.Timeout * 60);
PutItem(key, data, cache);
}
else
{
if (!(lockId is long) || data.LockId != (long) lockId)
throw new IgniteException(string.Format(CultureInfo.InvariantCulture,
"Invalid session release request, expected lockId: {0}, actual: {1}", data.LockId, lockId));
SetAndUnlockItem(key, data);
}
}
/// <summary>
/// Deletes item data from the session data store.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
/// <param name="id">The session identifier for the current request.</param>
/// <param name="lockId">The lock identifier for the current request.</param>
/// <param name="item">The <see cref="System.Web.SessionState.SessionStateStoreData" /> that represents
/// the item to delete from the data store.</param>
public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item)
{
RemoveItem(GetKey(id));
}
/// <summary>
/// Updates the expiration date and time of an item in the session data store.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
/// <param name="id">The session identifier for the current request.</param>
public override void ResetItemTimeout(HttpContext context, string id)
{
// No-op.
// This is not necessary since ResetItemTimeout is called right after SetAndReleaseItemExclusive,
// which itself resets the timeout when the item is inserted into cache.
}
/// <summary>
/// Creates a new <see cref="System.Web.SessionState.SessionStateStoreData" /> object to be used
/// for the current request.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
/// <param name="timeout">The session-state <see cref="System.Web.SessionState.HttpSessionState.Timeout" />
/// value for the new <see cref="System.Web.SessionState.SessionStateStoreData" />, in minutes.</param>
/// <returns>
/// A new <see cref="System.Web.SessionState.SessionStateStoreData" /> for the current request.
/// </returns>
public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
{
return new IgniteSessionStateStoreData(SessionStateUtility.GetSessionStaticObjects(context), timeout);
}
/// <summary>
/// Adds a new session-state item to the data store.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
/// <param name="id">The <see cref="System.Web.SessionState.HttpSessionState.SessionID" />
/// for the current request.</param>
/// <param name="timeout">The session <see cref="System.Web.SessionState.HttpSessionState.Timeout" />
/// for the current request, in minutes.</param>
public override void CreateUninitializedItem(HttpContext context, string id, int timeout)
{
var cache = _expiryCacheHolder.GetCacheWithExpiry((long) timeout * 60);
var key = GetKey(id);
var data = new IgniteSessionStateStoreData(SessionStateUtility.GetSessionStaticObjects(context), timeout);
PutItem(key, data, cache);
}
/// <summary>
/// Called by the <see cref="System.Web.SessionState.SessionStateModule" /> object at the end of a request.
/// </summary>
/// <param name="context">The <see cref="System.Web.HttpContext" /> for the current request.</param>
public override void EndRequest(HttpContext context)
{
// No-op.
}
/// <summary>
/// Gets the cache.
/// </summary>
private ICache<string, IgniteSessionStateStoreData> Cache
{
get
{
var holder = _expiryCacheHolder;
if (holder == null)
throw new InvalidOperationException(GetType() + " has not been initialized.");
return holder.Cache;
}
}
/// <summary>
/// Gets the key.
/// </summary>
private string GetKey(string sessionId)
{
return _applicationId == null ? sessionId : ApplicationId + "." + sessionId;
}
/// <summary>
/// Writes the lock info.
/// </summary>
private void WriteLockInfo(IBinaryRawWriter writer, long lockId, bool writeTime = false)
{
writer.WriteGuid(Cache.Ignite.GetCluster().GetLocalNode().Id);
writer.WriteLong(lockId);
if (writeTime)
writer.WriteTimestamp(DateTime.UtcNow);
}
/// <summary>
/// Locks the item.
/// </summary>
private SessionStateLockResult LockItem(string key, long lockId)
{
return OutInOp(Op.Lock,
w =>
{
w.WriteString(key);
WriteLockInfo(w, lockId, true);
},
r => new SessionStateLockResult(r));
}
/// <summary>
/// Unlocks the item.
/// </summary>
private void UnlockItem(string key, long lockId)
{
OutOp(Op.SetAndUnlock,
w =>
{
w.WriteString(key);
w.WriteBoolean(false); // Only unlock.
WriteLockInfo(w, lockId);
});
}
/// <summary>
/// Sets and unlocks the item.
/// </summary>
private void SetAndUnlockItem(string key, IgniteSessionStateStoreData data)
{
var cache = _expiryCacheHolder.GetCacheWithExpiry(data.Timeout * 60);
OutOp(Op.SetAndUnlock, w =>
{
w.WriteString(key);
w.WriteBoolean(true); // Unlock and update.
data.WriteBinary(w, true);
}, cache);
}
/// <summary>
/// Puts the item.
/// </summary>
private void PutItem(string key, IgniteSessionStateStoreData data, ICache<string, IgniteSessionStateStoreData> cache)
{
OutOp(Op.Put, w =>
{
w.WriteString(key);
data.WriteBinary(w, false);
}, cache);
}
/// <summary>
/// Gets the item.
/// </summary>
private IgniteSessionStateStoreData GetItem(string key)
{
return OutInOp(Op.Get, w => w.WriteString(key), r => new IgniteSessionStateStoreData(r));
}
/// <summary>
/// Removes the item.
/// </summary>
private void RemoveItem(string key)
{
OutOp(Op.Remove, w => w.WriteString(key));
}
/// <summary>
/// Invokes the extension operation.
/// </summary>
private void OutOp(Op op, Action<IBinaryRawWriter> writeAction,
ICache<string, IgniteSessionStateStoreData> cache = null)
{
OutInOp<object>(op, writeAction, null, cache);
}
/// <summary>
/// Invokes the extension operation.
/// </summary>
private T OutInOp<T>(Op op, Action<IBinaryRawWriter> writeAction, Func<IBinaryRawReader, T> readFunc,
ICache<string, IgniteSessionStateStoreData> cache = null)
{
cache = cache ?? Cache;
return ((ICacheInternal) cache).DoOutInOpExtension(ExtensionId, (int) op, writeAction, readFunc);
}
}
}