├── .gitignore ├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── MemcachedSessionProvider ├── packages.config ├── MemcachedSessionProvider.nuspec ├── Properties │ └── AssemblyInfo.cs ├── SessionNodeLocator.cs ├── SessionData.cs ├── SessionKeyFormat.cs ├── MemcachedSessionProvider.csproj ├── SessionCacheWithBackup.cs ├── SessionNodeLocatorImpl.cs └── SessionProvider.cs ├── MemcachedSessionProvider.Tests ├── packages.config ├── SessionDataTests.cs ├── Properties │ └── AssemblyInfo.cs ├── App.config ├── SessionCacheWithBackupTests.cs ├── MemcachedSessionProvider.Tests.csproj ├── SessionBackupTests.cs ├── SessionKeyFormatTests.cs ├── MockMemcachedClient.cs └── SessionNodeLocatorTests.cs ├── MemcachedSessionProvider.sln ├── .gitattributes ├── README.markdown └── License.txt /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj 2 | [Bb]in 3 | packages 4 | *.user 5 | *.suo 6 | *.cache 7 | *.nupkg -------------------------------------------------------------------------------- /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohita/MemcachedSessionProvider/HEAD/.nuget/NuGet.exe -------------------------------------------------------------------------------- /MemcachedSessionProvider/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /MemcachedSessionProvider/MemcachedSessionProvider.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $id$ 5 | $version$ 6 | $title$ 7 | $author$ 8 | $author$ 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | https://github.com/rohita/MemcachedSessionProvider 11 | false 12 | $description$ 13 | Copyright 2014 Rohit Agarwal 14 | asp.net session provider sessionstate memcached 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/SessionDataTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Web; 7 | using System.Web.Hosting; 8 | using System.Web.SessionState; 9 | using NUnit.Framework; 10 | 11 | namespace MemcachedSessionProvider.Tests 12 | { 13 | [TestFixture] 14 | public class SessionDataTests 15 | { 16 | [Test] 17 | public void TestDeserializeHandlesNull() 18 | { 19 | var request = new SimpleWorkerRequest("", "", "", null, new StringWriter()); 20 | var context = new HttpContext(request); 21 | var s = new SessionData(SessionStateActions.None, 10); 22 | Assert.DoesNotThrow(() => s.Deserialize(context)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("MemcachedSessionProvider.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("MemcachedSessionProvider.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © Rohit Agarwal 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("35c30248-2cdd-4a34-ac18-4749aeeb350b")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /MemcachedSessionProvider/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("MemcachedSessionProvider")] 9 | [assembly: AssemblyDescription("A highly available, high performance ASP.NET session state store provider using Memcached.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Rohit Agarwal")] 12 | [assembly: AssemblyProduct("MemcachedSessionProvider")] 13 | [assembly: AssemblyCopyright("Copyright © Rohit Agarwal 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | [assembly: InternalsVisibleTo("MemcachedSessionProvider.Tests")] 17 | 18 | // Setting ComVisible to false makes the types in this assembly not visible 19 | // to COM components. If you need to access a type in this assembly from 20 | // COM, set the ComVisible attribute to true on that type. 21 | [assembly: ComVisible(false)] 22 | 23 | // The following GUID is for the ID of the typelib if this project is exposed to COM 24 | [assembly: Guid("c2af4fab-dc61-43d7-824e-fd4a26be2098")] 25 | 26 | // Version information for an assembly consists of the following four values: 27 | // 28 | // Major Version 29 | // Minor Version 30 | // Build Number 31 | // Revision 32 | // 33 | // You can specify all the values or you can default the Build and Revision Numbers 34 | // by using the '*' as shown below: 35 | // [assembly: AssemblyVersion("1.0.*")] 36 | [assembly: AssemblyVersion("1.0.5.0")] 37 | [assembly: AssemblyFileVersion("1.0.5.0")] 38 | 39 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EAF584A0-A5F1-47BF-BC54-004C3978ACE8}" 5 | ProjectSection(SolutionItems) = preProject 6 | License.txt = License.txt 7 | README.markdown = README.markdown 8 | EndProjectSection 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemcachedSessionProvider", "MemcachedSessionProvider\MemcachedSessionProvider.csproj", "{B062D12C-EFB7-4CB5-B560-FB2387468E82}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemcachedSessionProvider.Tests", "MemcachedSessionProvider.Tests\MemcachedSessionProvider.Tests.csproj", "{B1948D1F-D924-4E91-AAEA-41D47EF602D0}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{74C320A6-C406-4280-9813-5B411CAA1EB1}" 15 | ProjectSection(SolutionItems) = preProject 16 | .nuget\NuGet.Config = .nuget\NuGet.Config 17 | .nuget\NuGet.exe = .nuget\NuGet.exe 18 | .nuget\NuGet.targets = .nuget\NuGet.targets 19 | EndProjectSection 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {B062D12C-EFB7-4CB5-B560-FB2387468E82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {B062D12C-EFB7-4CB5-B560-FB2387468E82}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {B062D12C-EFB7-4CB5-B560-FB2387468E82}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {B062D12C-EFB7-4CB5-B560-FB2387468E82}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {B1948D1F-D924-4E91-AAEA-41D47EF602D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {B1948D1F-D924-4E91-AAEA-41D47EF602D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {B1948D1F-D924-4E91-AAEA-41D47EF602D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {B1948D1F-D924-4E91-AAEA-41D47EF602D0}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /MemcachedSessionProvider/SessionNodeLocator.cs: -------------------------------------------------------------------------------- 1 | #region [License] 2 | /* ************************************************************ 3 | * 4 | * Copyright (c) 2014 Rohit Agarwal 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * ************************************************************/ 19 | #endregion 20 | 21 | using System.Collections.Generic; 22 | using Enyim.Caching.Memcached; 23 | 24 | namespace MemcachedSessionProvider 25 | { 26 | /// 27 | /// This is a custom implementation of the interface 28 | /// in the Enyim.Caching.Memcached library. This handles keys with prefix "bak:". 29 | /// These backup keys are stored on the "next" available server. 30 | /// 31 | internal class SessionNodeLocator : IMemcachedNodeLocator 32 | { 33 | 34 | public void Initialize(IList nodes) 35 | { 36 | SessionCacheWithBackup.Instance.InitializeLocator(nodes); 37 | } 38 | 39 | public IMemcachedNode Locate(string key) 40 | { 41 | return SessionCacheWithBackup.Instance.Locate(key); 42 | } 43 | 44 | public IEnumerable GetWorkingNodes() 45 | { 46 | return SessionCacheWithBackup.Instance.GetWorkingNodes(); 47 | } 48 | 49 | internal void AssignPrimaryBackupNodes(string key) 50 | { 51 | SessionCacheWithBackup.Instance.AssignPrimaryBackupNodes(key); 52 | } 53 | 54 | internal void Reset() 55 | { 56 | SessionCacheWithBackup.Instance.ResetLocator(); 57 | } 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /MemcachedSessionProvider/SessionData.cs: -------------------------------------------------------------------------------- 1 | #region [License] 2 | /* ************************************************************ 3 | * 4 | * Copyright (c) 2014 Rohit Agarwal 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * ************************************************************/ 19 | #endregion 20 | 21 | using System; 22 | using System.IO; 23 | using System.Web; 24 | using System.Web.SessionState; 25 | 26 | namespace MemcachedSessionProvider 27 | { 28 | [Serializable] 29 | internal class SessionData 30 | { 31 | private readonly int _actionFlag; 32 | private readonly int _timeout; 33 | private byte[] _serializedSessionData; 34 | 35 | public int Timeout { get { return _timeout; } } 36 | public long SavedAt { get; set; } 37 | 38 | public SessionData(SessionStateActions actionFlag, int timeout) 39 | { 40 | _actionFlag = (int) actionFlag; 41 | _timeout = timeout; 42 | _serializedSessionData = null; 43 | } 44 | 45 | public void Serialize(SessionStateItemCollection items) 46 | { 47 | var ms = new MemoryStream(); 48 | var writer = new BinaryWriter(ms); 49 | 50 | if (items != null) 51 | items.Serialize(writer); 52 | 53 | writer.Close(); 54 | 55 | _serializedSessionData = ms.ToArray(); 56 | } 57 | 58 | public SessionStateStoreData Deserialize(HttpContext context) 59 | { 60 | var ms = _serializedSessionData == null 61 | ? new MemoryStream() 62 | : new MemoryStream(_serializedSessionData); 63 | 64 | var sessionItems = new SessionStateItemCollection(); 65 | 66 | if (ms.Length > 0) 67 | { 68 | var reader = new BinaryReader(ms); 69 | sessionItems = SessionStateItemCollection.Deserialize(reader); 70 | } 71 | 72 | return new SessionStateStoreData(sessionItems, 73 | SessionStateUtility.GetSessionStaticObjects(context), 74 | _timeout); 75 | } 76 | 77 | public SessionStateActions GetActionFlag() 78 | { 79 | return (SessionStateActions) _actionFlag; 80 | } 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /MemcachedSessionProvider/SessionKeyFormat.cs: -------------------------------------------------------------------------------- 1 | #region [License] 2 | /* ************************************************************ 3 | * 4 | * Copyright (c) 2014 Rohit Agarwal 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * ************************************************************/ 19 | #endregion 20 | 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Linq; 24 | using System.Text; 25 | using System.Web; 26 | 27 | namespace MemcachedSessionProvider 28 | { 29 | internal class SessionKeyFormat 30 | { 31 | 32 | private string _applicationName; 33 | private const string AspSessionPrefix = "__AspSession_"; 34 | private const string BackupPrefix = "bak:"; 35 | private const string Format = "{0}{1}{2}{3}"; 36 | 37 | public SessionKeyFormat() 38 | { 39 | if (!string.IsNullOrEmpty(HttpRuntime.AppDomainAppId)) 40 | { 41 | _applicationName = string.Concat(HttpRuntime.AppDomainAppId, "_"); 42 | } 43 | 44 | } 45 | 46 | public SessionKeyFormat(string applicationName) 47 | { 48 | if (!string.IsNullOrEmpty(applicationName)) 49 | { 50 | _applicationName = string.Concat(applicationName, "_"); 51 | } 52 | } 53 | 54 | public String GetBackupKey(String key) 55 | { 56 | if (IsBackupKey(key)) 57 | { 58 | return key; 59 | } 60 | 61 | if (IsPrimaryKey(key)) 62 | { 63 | return String.Format("{0}{1}", BackupPrefix, key); 64 | } 65 | 66 | return String.Format(Format, BackupPrefix, AspSessionPrefix, _applicationName, key); 67 | } 68 | 69 | public bool IsBackupKey(String key) 70 | { 71 | return key.StartsWith(BackupPrefix); 72 | } 73 | 74 | public bool IsPrimaryKey(string key) 75 | { 76 | return key.StartsWith(AspSessionPrefix); 77 | } 78 | 79 | public String GetPrimaryKey(String key) 80 | { 81 | if (IsPrimaryKey(key)) 82 | { 83 | return key; 84 | } 85 | 86 | if (IsBackupKey(key)) 87 | { 88 | return key.Substring(BackupPrefix.Length); 89 | } 90 | 91 | return String.Format(Format, string.Empty, AspSessionPrefix, _applicationName, key); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/SessionCacheWithBackupTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using Enyim.Caching.Memcached; 7 | using NUnit.Framework; 8 | using System.Web.SessionState; 9 | 10 | namespace MemcachedSessionProvider.Tests 11 | { 12 | [TestFixture] 13 | public class SessionCacheWithBackupTests 14 | { 15 | private SessionCacheWithBackup cache = SessionCacheWithBackup.Instance; 16 | private const string SessionId = "abc"; 17 | private string _primaryKey = new SessionKeyFormat(null).GetPrimaryKey(SessionId); 18 | private string _backupKey = new SessionKeyFormat(null).GetBackupKey(SessionId); 19 | 20 | 21 | 22 | [Test] 23 | public void StoreSessionTest() 24 | { 25 | cache.ResetMemcachedClient("sessionManagement/memcached"); 26 | cache.Remove(SessionId); 27 | 28 | cache.Store(SessionId, new SessionData(SessionStateActions.None, 30), TimeSpan.FromMinutes(30)); 29 | 30 | var data = cache.GetByCacheKey(_primaryKey); 31 | Assert.NotNull(data); 32 | 33 | var data2 = cache.GetByCacheKey(_backupKey); 34 | Assert.NotNull(data2); 35 | 36 | var firstStore = data.SavedAt; 37 | cache.Store(SessionId, data, TimeSpan.FromMinutes(30)); 38 | data = cache.GetByCacheKey(_primaryKey); 39 | var secondStore = data.SavedAt; 40 | Assert.Greater(secondStore, firstStore); 41 | 42 | cache.Remove(SessionId); 43 | } 44 | 45 | [Test] 46 | public void RemoveSessionTest() 47 | { 48 | cache.ResetMemcachedClient("sessionManagement/memcached"); 49 | 50 | cache.Store(SessionId, new SessionData(SessionStateActions.None, 30), TimeSpan.FromMinutes(30)); 51 | 52 | var data = cache.GetByCacheKey(_primaryKey); 53 | var data2 = cache.GetByCacheKey(_backupKey); 54 | Assert.NotNull(data); 55 | Assert.NotNull(data2); 56 | 57 | cache.Remove(SessionId); 58 | 59 | data = cache.GetByCacheKey(_primaryKey); 60 | data2 = cache.GetByCacheKey(_backupKey); 61 | Assert.IsNull(data); 62 | Assert.IsNull(data2); 63 | } 64 | 65 | [Test] 66 | public void DefaultLocatorTest() 67 | { 68 | cache.ResetMemcachedClient("test/defaultLocator"); 69 | 70 | cache.Store(SessionId, new SessionData(SessionStateActions.None, 30), TimeSpan.FromMinutes(30)); 71 | 72 | var data = cache.GetByCacheKey(_primaryKey); 73 | var data2 = cache.GetByCacheKey(_backupKey); 74 | Assert.NotNull(data); 75 | Assert.IsNull(data2, "Don't backup sessions if not using backup node locator"); 76 | 77 | cache.Remove(SessionId); 78 | 79 | } 80 | 81 | [Test] 82 | public void SingleNodeTest() 83 | { 84 | cache.ResetMemcachedClient("test/singleServer"); 85 | 86 | cache.Store(SessionId, new SessionData(SessionStateActions.None, 30), TimeSpan.FromMinutes(30)); 87 | 88 | var data = cache.GetByCacheKey(_primaryKey); 89 | var data2 = cache.GetByCacheKey(_backupKey); 90 | Assert.NotNull(data); 91 | Assert.IsNull(data2, "Don't backup sessions if using single node"); 92 | 93 | cache.Remove(SessionId); 94 | 95 | } 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MemcachedSessionProvider/MemcachedSessionProvider.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Release 5 | AnyCPU 6 | 9.0.21022 7 | 2.0 8 | {B062D12C-EFB7-4CB5-B560-FB2387468E82} 9 | Library 10 | Properties 11 | MemcachedSessionProvider 12 | MemcachedSessionProvider 13 | v3.5 14 | 512 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 3.5 28 | 29 | 30 | ..\ 31 | true 32 | 33 | 34 | true 35 | full 36 | false 37 | bin\Debug\ 38 | DEBUG;TRACE 39 | prompt 40 | 4 41 | 42 | 43 | false 44 | 45 | 46 | pdbonly 47 | true 48 | bin\Release\ 49 | TRACE 50 | prompt 51 | 4 52 | false 53 | bin\Release\MemcachedSessionProvider.xml 54 | 55 | 56 | 57 | False 58 | ..\packages\EnyimMemcached.2.12\lib\net35\Enyim.Caching.dll 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 85 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/MemcachedSessionProvider.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B1948D1F-D924-4E91-AAEA-41D47EF602D0} 8 | Library 9 | Properties 10 | MemcachedSessionProvider.Tests 11 | MemcachedSessionProvider.Tests 12 | v3.5 13 | 512 14 | 15 | ..\ 16 | true 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | False 38 | ..\packages\EnyimMemcached.2.12\lib\net35\Enyim.Caching.dll 39 | 40 | 41 | ..\packages\NUnit.2.6.3\lib\nunit.framework.dll 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {b062d12c-efb7-4cb5-b560-fb2387468e82} 68 | MemcachedSessionProvider 69 | 70 | 71 | 72 | 73 | 80 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Memcached Session Provider 2 | 3 | This is a highly available, high performance ASP.NET session state store provider using Memcached. 4 | 5 | Features: 6 | 7 | * Handles Memcached node failures 8 | * No session locking per request 9 | 10 | ### Handling node failures 11 | In a pool of memcached nodes, sessions are distributed equally across all the nodes. Additionally, for every session, a backup 12 | copy is stored on a secondary memcached node. This is similar to the way [Memcached session manager for Tomcat](https://code.google.com/p/memcached-session-manager/) is implemented. 13 | 14 | Example: In a pool of 2 memcached nodes, if a session `S1` gets stored on memcached node `M1`, the backup session `bak:S1` is stored 15 | on node `M2`. If node `M1` goes down, session is not lost. It will be retrived from the `M2` node without any interruption. 16 | Similarly, `M1` acts as backup node for `M2`, in case `M2` goes down. 17 | ``` 18 | 19 | S1 S2 20 | bak:S2 bak:S1 21 | ``` 22 | Note that if only 1 memcached node is configured then there is no backup. 23 | 24 | ### No session locking 25 | [Session locking in ASP.NET](http://msdn.microsoft.com/en-us/library/ms178587.aspx) can cause a few 26 | [performance problems](http://stackoverflow.com/questions/3629709/i-just-discovered-why-all-asp-net-websites-are-slow-and-i-am-trying-to-work-out). 27 | This custom implementation of Session provider does not lock any Session. This should not be a problem in most cases. But if 28 | you need locked, consistent data as part of the session, you can implement a lock/concurrency check in your application, 29 | or use a different Memcached provider (see [here](https://github.com/enyim/memcached-providers) and [here](http://memcachedproviders.codeplex.com/)). 30 | 31 | ## Requirements 32 | You'll need .NET Framework 3.5 or later to use the precompiled binaries. To build client, you'll need Visual Studio 2012. 33 | 34 | ## Install 35 | In your web project, include the assembly via [NuGet Package](https://www.nuget.org/packages/MemcachedSessionProvider/). 36 | 37 | ## Web.config 38 | This library uses the [Enyim Memcached client](https://github.com/enyim/EnyimMemcached). Make the following changes in 39 | the web.config file for Enyim client 40 | ```xml 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ``` 61 | #### memcached/locator 62 | The `memcached/locator` is used to map objects to servers in the pool. Replace the default implementation with the 63 | type `MemcachedSessionProvider.SessionNodeLocator, MemcachedSessionProvider`. This handles the session backup. 64 | 65 | See here for [more configuration options for Enyim Memcached](https://github.com/enyim/EnyimMemcached/wiki/MemcachedClient-Configuration) 66 | 67 | Also make the following change in web.config to use the custom Session State provider 68 | ```xml 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ``` 83 | 84 | ## Questions? 85 | If you have questions, bugs reports, or feature requests, please submit them via the [Issue Tracker](https://github.com/rohita/MemcachedSessionProvider/issues). 86 | 87 | ## Reference 88 | This implementation is based on the [sample provided by Microsoft](http://msdn.microsoft.com/en-us/library/ms178588.aspx). 89 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/SessionBackupTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using System.Web.SessionState; 7 | using Enyim.Caching.Configuration; 8 | using Enyim.Caching.Memcached; 9 | using NUnit.Framework; 10 | 11 | namespace MemcachedSessionProvider.Tests 12 | { 13 | [TestFixture] 14 | public class SessionBackupTests 15 | { 16 | private const string N2Key = "abcdefgh_34567890"; 17 | private const string N1Key = "abcdefgh"; 18 | private const string N2Key2Node = "345678906789"; 19 | ISocketPoolConfiguration _s = new SocketPoolConfiguration(); 20 | private SessionCacheWithBackup cache = SessionCacheWithBackup.Instance; 21 | 22 | [Test] 23 | public void TestSingleClientPrimaryDown() 24 | { 25 | var n1 = GetNode(1); 26 | var n2 = GetNode(2); 27 | var n3 = GetNode(3); 28 | var n4 = GetNode(4); 29 | 30 | var newClient = new MockMemcachedClient(new List { n1, n2, n3, n4 }); 31 | cache.ResetMemcachedClient(newClient, null); 32 | 33 | cache.Store(N1Key, new SessionData(SessionStateActions.None, 23), TimeSpan.FromMinutes(30)); 34 | 35 | newClient.SetNodeDead(n1, new List { n2, n3, n4 }); 36 | 37 | var data = cache.Get(N1Key); 38 | Assert.AreEqual(23, data.Timeout); 39 | } 40 | 41 | [Test] 42 | public void TestSingleClientPrimaryUp() 43 | { 44 | var n1 = GetNode(1); 45 | var n2 = GetNode(2); 46 | var n3 = GetNode(3); 47 | var n4 = GetNode(4); 48 | var nodes = new List {n1, n2, n3, n4}; 49 | 50 | var newClient = new MockMemcachedClient(nodes); 51 | cache.ResetMemcachedClient(newClient, null); 52 | 53 | cache.Store(N1Key, new SessionData(SessionStateActions.None, 23), TimeSpan.FromMinutes(30)); 54 | 55 | newClient.SetNodeDead(n1, new List { n2, n3, n4 }); 56 | 57 | var data = cache.Get(N1Key); 58 | Assert.AreEqual(23, data.Timeout); 59 | 60 | newClient.SetNodeAlive(n1, new List { n1, n2, n3, n4 }); 61 | 62 | data = cache.Get(N1Key); 63 | Assert.AreEqual(23, data.Timeout); 64 | } 65 | 66 | [Test] 67 | public void TestSingleClientPrimaryDown2NdStore() 68 | { 69 | var n1 = GetNode(1); 70 | var n2 = GetNode(2); 71 | var n3 = GetNode(3); 72 | var n4 = GetNode(4); 73 | 74 | var newClient = new MockMemcachedClient(new List { n1, n2, n3, n4 }); 75 | cache.ResetMemcachedClient(newClient, null); 76 | 77 | cache.Store(N1Key, new SessionData(SessionStateActions.None, 23), TimeSpan.FromMinutes(30)); 78 | 79 | newClient.SetNodeDead(n1, new List { n2, n3, n4 }); 80 | 81 | cache.Store(N1Key, new SessionData(SessionStateActions.None, 25), TimeSpan.FromMinutes(30)); 82 | 83 | var data = cache.Get(N1Key); 84 | Assert.AreEqual(25, data.Timeout); 85 | } 86 | 87 | 88 | [Test] 89 | public void TestTwoClientsPrimaryDown() 90 | { 91 | var n1 = GetNode(1); 92 | var n2 = GetNode(2); 93 | var n3 = GetNode(3); 94 | var n4 = GetNode(4); 95 | 96 | var s1 = new SessionNodeLocatorImpl(); 97 | s1.Initialize(new List { n1, n2, n3, n4 }); 98 | var newClient = new MockMemcachedClient(new List { n1, n2, n3, n4 }); 99 | cache.ResetMemcachedClient(newClient, s1); 100 | 101 | cache.Store(N1Key, new SessionData(SessionStateActions.None, 23), TimeSpan.FromMinutes(30)); 102 | 103 | newClient.SetNodeDead(n1, new List { n2, n3, n4 }); 104 | 105 | cache.Store(N1Key, new SessionData(SessionStateActions.None, 25), TimeSpan.FromMinutes(30)); 106 | 107 | var s2 = new SessionNodeLocatorImpl(); 108 | s2.Initialize(new List { n1, n2, n3, n4 }); 109 | cache.ResetMemcachedClient(newClient, s2); 110 | newClient.SetNodeDead(n1, new List { n2, n3, n4 }); 111 | 112 | var data = cache.Get(N1Key); 113 | Assert.AreEqual(25, data.Timeout); 114 | } 115 | 116 | private IMemcachedNode GetNode(int num) 117 | { 118 | var p1 = new IPEndPoint(IPAddress.Parse(string.Format("10.10.10.{0}", num)), 11211); 119 | return new MemcachedNode(p1, _s); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/SessionKeyFormatTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Web; 7 | using NUnit.Framework; 8 | 9 | namespace MemcachedSessionProvider.Tests 10 | { 11 | [TestFixture] 12 | public class SessionKeyFormatTests 13 | { 14 | [TestCase("bak:__AspSession_testapp_abcd456", "bak:__AspSession_testapp_abcd456")] 15 | [TestCase("bak:__AspSession_testapp_35c30248-2cdd-4a34-ac18-4749aeeb350b", "35c30248-2cdd-4a34-ac18-4749aeeb350b")] 16 | [TestCase("bak:__AspSession_testapp_abcd456", "__AspSession_testapp_abcd456")] 17 | [TestCase("bak:__AspSession_testapp_", "")] 18 | [TestCase("bak:__AspSession_testapp_", "bak:__AspSession_testapp_")] 19 | public void TestGetBackupKey(string expected, string sessionId) 20 | { 21 | var s = new SessionKeyFormat("testapp"); 22 | Assert.AreEqual(expected, s.GetBackupKey(sessionId)); 23 | } 24 | 25 | [TestCase("bak:__AspSession_35c30248-2cdd-4a34-ac18-4749aeeb350b", "35c30248-2cdd-4a34-ac18-4749aeeb350b")] 26 | [TestCase("bak:__AspSession__abcd456", "__AspSession__abcd456")] 27 | [TestCase("bak:__AspSession_", "")] 28 | public void TestGetBackupKey2(string expected, string sessionId) 29 | { 30 | var s = new SessionKeyFormat(null); 31 | Assert.AreEqual(expected, s.GetBackupKey(sessionId)); 32 | } 33 | 34 | [TestCase("__AspSession_testapp_abcd456", "bak:__AspSession_testapp_abcd456")] 35 | [TestCase("__AspSession_testapp_35c30248-2cdd-4a34-ac18-4749aeeb350b", "35c30248-2cdd-4a34-ac18-4749aeeb350b")] 36 | [TestCase("__AspSession_testapp_abcd456", "__AspSession_testapp_abcd456")] 37 | [TestCase("__AspSession_testapp_", "")] 38 | [TestCase("__AspSession_testapp_", "bak:__AspSession_testapp_")] 39 | public void TestGetPrimaryKey(string expected, string sessionId) 40 | { 41 | var s = new SessionKeyFormat("testapp"); 42 | Assert.AreEqual(expected, s.GetPrimaryKey(sessionId)); 43 | } 44 | 45 | [TestCase("__AspSession_testapp_abcd456", "bak:__AspSession_testapp_abcd456")] 46 | [TestCase("__AspSession_35c30248-2cdd-4a34-ac18-4749aeeb350b", "35c30248-2cdd-4a34-ac18-4749aeeb350b")] 47 | [TestCase("__AspSession_testapp_abcd456", "__AspSession_testapp_abcd456")] 48 | [TestCase("__AspSession_", "")] 49 | [TestCase("__AspSession__abcd456", "__AspSession__abcd456")] 50 | public void TestGetPrimaryKey2(string expected, string sessionId) 51 | { 52 | var s = new SessionKeyFormat(""); 53 | Assert.AreEqual(expected, s.GetPrimaryKey(sessionId)); 54 | } 55 | 56 | [TestCase("__AspSession_/LM/W3SVC/1/ROOT/ApexUI_02l2seqes0hrgaffs5hgfg55", "/LM/W3SVC/1/ROOT/ApexUI_02l2seqes0hrgaffs5hgfg55")] 57 | public void TestGetPrimaryKey3(string expected, string sessionId) 58 | { 59 | var s = new SessionKeyFormat(null); 60 | Assert.AreEqual(expected, s.GetPrimaryKey(sessionId)); 61 | } 62 | 63 | [TestCase(false, "abcd456")] 64 | [TestCase(false, "")] 65 | [TestCase(true, "bak:__AspSession_testapp_abcd456")] 66 | [TestCase(true, "bak:__AspSession__abcd456")] 67 | [TestCase(true, "bak:__AspSession__")] 68 | [TestCase(false, "__AspSession_testapp_abcd456")] 69 | [TestCase(false, "__AspSession__abcd456")] 70 | [TestCase(false, "__AspSession__")] 71 | public void TestIsBackupKey(bool expected, string sessionId) 72 | { 73 | var s = new SessionKeyFormat(null); 74 | Assert.AreEqual(expected, s.IsBackupKey(sessionId)); 75 | } 76 | 77 | [TestCase(false, "abcd456")] 78 | [TestCase(false, "")] 79 | [TestCase(false, "bak:__AspSession_testapp_abcd456")] 80 | [TestCase(false, "bak:__AspSession__abcd456")] 81 | [TestCase(false, "bak:__AspSession__")] 82 | [TestCase(true, "__AspSession_testapp_abcd456")] 83 | [TestCase(true, "__AspSession__abcd456")] 84 | [TestCase(true, "__AspSession__")] 85 | public void TestIsPrimaryKey(bool expected, string sessionId) 86 | { 87 | var s = new SessionKeyFormat("testapp"); 88 | Assert.AreEqual(expected, s.IsPrimaryKey(sessionId)); 89 | } 90 | 91 | [TestCase(true, "__AspSession_testapp_abcd456")] 92 | [TestCase(true, "__AspSession__abcd456")] 93 | [TestCase(true, "__AspSession__")] 94 | public void TestIsPrimaryKey2(bool expected, string sessionId) 95 | { 96 | var s = new SessionKeyFormat(null); 97 | Assert.AreEqual(expected, s.IsPrimaryKey(sessionId)); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /MemcachedSessionProvider/SessionCacheWithBackup.cs: -------------------------------------------------------------------------------- 1 | #region [License] 2 | /* ************************************************************ 3 | * 4 | * Copyright (c) 2014 Rohit Agarwal 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * ************************************************************/ 19 | #endregion 20 | 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Configuration; 24 | using System.Reflection; 25 | using System.Threading; 26 | using Enyim.Caching; 27 | using Enyim.Caching.Configuration; 28 | using Enyim.Caching.Memcached; 29 | 30 | namespace MemcachedSessionProvider 31 | { 32 | internal sealed class SessionCacheWithBackup 33 | { 34 | private static readonly SessionCacheWithBackup _instance = new SessionCacheWithBackup(); 35 | private volatile IMemcachedClient _client; 36 | private SessionKeyFormat _sessionKeyFormat; 37 | private const string DefaultConfigSection = "sessionManagement/memcached"; 38 | private MemcachedClientSection _memcachedClientSection; 39 | private SessionNodeLocatorImpl _locatorImpl; 40 | private object _clientAccessLock = new object(); 41 | 42 | private SessionCacheWithBackup() 43 | { 44 | _memcachedClientSection = ConfigurationManager.GetSection(DefaultConfigSection) as MemcachedClientSection; 45 | _locatorImpl = new SessionNodeLocatorImpl(); 46 | _sessionKeyFormat = new SessionKeyFormat(); 47 | } 48 | 49 | public static SessionCacheWithBackup Instance 50 | { 51 | get { return _instance; } 52 | } 53 | 54 | public void InitializeClient() 55 | { 56 | if (_client != null) return; 57 | 58 | lock (_clientAccessLock) 59 | { 60 | if (_client == null) 61 | _client = new MemcachedClient(_memcachedClientSection); 62 | } 63 | } 64 | 65 | public SessionData Get(string sessionId) 66 | { 67 | var cacheKey = _sessionKeyFormat.GetPrimaryKey(sessionId); 68 | var data = _client.Get(cacheKey); 69 | 70 | if (data != null) 71 | { 72 | // Check if a new primary was assigned 73 | data = CheckForNewer(sessionId, data); 74 | } 75 | else if (IsBackupEnabled()) // try backup 76 | { 77 | var backupKey = _sessionKeyFormat.GetBackupKey(sessionId); 78 | data = _client.Get(backupKey); 79 | 80 | if (data != null) 81 | { 82 | // Data found on backup node. This means primary is down. 83 | // Check on new primary 84 | data = CheckForNewer(sessionId, data); 85 | } 86 | } 87 | 88 | return data; 89 | } 90 | 91 | private SessionData CheckForNewer(string sessionId, SessionData data) 92 | { 93 | string cacheKey = _sessionKeyFormat.GetPrimaryKey(sessionId); 94 | 95 | // try if there is fresher copy 96 | // This happens when a node goes up or down 97 | if (AssignPrimaryBackupNodes(cacheKey)) 98 | { 99 | var newData = _client.Get(cacheKey); 100 | if (newData == null || data.SavedAt > newData.SavedAt) 101 | { 102 | // If not found or older, that means this client hit the key first 103 | // so relocate session for next call 104 | Store(sessionId, data, TimeSpan.FromMinutes(data.Timeout)); 105 | } 106 | else 107 | { 108 | // else found newer, that means some other client already updated this 109 | data = newData; 110 | } 111 | } 112 | 113 | return data; 114 | } 115 | 116 | public void Store(string sessionId, SessionData cacheItem, TimeSpan timeout) 117 | { 118 | var cacheKey = _sessionKeyFormat.GetPrimaryKey(sessionId); 119 | AssignPrimaryBackupNodes(cacheKey); 120 | 121 | cacheItem.SavedAt = DateTime.UtcNow.Ticks; 122 | _client.Store(StoreMode.Set, cacheKey, cacheItem, timeout); 123 | 124 | if (IsBackupEnabled()) // backup 125 | { 126 | var backupKey = _sessionKeyFormat.GetBackupKey(sessionId); 127 | _client.Store(StoreMode.Set, backupKey, cacheItem, timeout); 128 | } 129 | } 130 | 131 | public void Remove(string sessionId) 132 | { 133 | var cacheKey = _sessionKeyFormat.GetPrimaryKey(sessionId); 134 | _client.Remove(cacheKey); 135 | 136 | if (IsBackupEnabled()) 137 | { 138 | _client.Remove(_sessionKeyFormat.GetBackupKey(sessionId)); 139 | } 140 | } 141 | 142 | public void InitializeLocator(IList nodes) 143 | { 144 | _locatorImpl.Initialize(nodes); 145 | } 146 | 147 | public IMemcachedNode Locate(string key) 148 | { 149 | return _locatorImpl.Locate(key); 150 | } 151 | 152 | public IEnumerable GetWorkingNodes() 153 | { 154 | return _locatorImpl.GetWorkingNodes(); 155 | } 156 | 157 | private bool IsBackupEnabled() 158 | { 159 | return _memcachedClientSection.Servers.Count > 1 160 | && IsUsingSessionNodeLocator(); 161 | } 162 | 163 | private bool IsUsingSessionNodeLocator() 164 | { 165 | return _memcachedClientSection.NodeLocator.Type == typeof (SessionNodeLocator); 166 | } 167 | 168 | public bool AssignPrimaryBackupNodes(string cacheKey) 169 | { 170 | if (IsUsingSessionNodeLocator()) 171 | { 172 | return _locatorImpl.AssignPrimaryBackupNodes(cacheKey); 173 | } 174 | 175 | return false; 176 | } 177 | 178 | 179 | internal void ResetMemcachedClient(string memcachedConfigSection) 180 | { 181 | if (_client != null) 182 | { 183 | _client.Dispose(); 184 | _client = null; 185 | } 186 | _locatorImpl = new SessionNodeLocatorImpl(); 187 | _memcachedClientSection = ConfigurationManager.GetSection(memcachedConfigSection) as MemcachedClientSection; 188 | InitializeClient(); 189 | } 190 | 191 | internal void ResetMemcachedClient(IMemcachedClient newClient, SessionNodeLocatorImpl newLocator) 192 | { 193 | if (_client != null) _client.Dispose(); 194 | _client = newClient; 195 | _locatorImpl = newLocator ?? _locatorImpl; 196 | } 197 | 198 | internal void ResetLocator() 199 | { 200 | _locatorImpl = new SessionNodeLocatorImpl(); 201 | } 202 | 203 | internal SessionData GetByCacheKey(string key) 204 | { 205 | return _client.Get(key); 206 | } 207 | 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | 32 | 33 | 34 | 35 | $(SolutionDir).nuget 36 | 37 | 38 | 39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config 40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config 41 | 42 | 43 | 44 | $(MSBuildProjectDirectory)\packages.config 45 | $(PackagesProjectConfig) 46 | 47 | 48 | 49 | 50 | $(NuGetToolsPath)\NuGet.exe 51 | @(PackageSource) 52 | 53 | "$(NuGetExePath)" 54 | mono --runtime=v4.0.30319 "$(NuGetExePath)" 55 | 56 | $(TargetDir.Trim('\\')) 57 | 58 | -RequireConsent 59 | -NonInteractive 60 | 61 | "$(SolutionDir) " 62 | "$(SolutionDir)" 63 | 64 | 65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 67 | 68 | 69 | 70 | RestorePackages; 71 | $(BuildDependsOn); 72 | 73 | 74 | 75 | 76 | $(BuildDependsOn); 77 | BuildPackage; 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/MockMemcachedClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Reflection; 6 | using System.Text; 7 | using Enyim.Caching; 8 | using Enyim.Caching.Configuration; 9 | using Enyim.Caching.Memcached; 10 | 11 | namespace MemcachedSessionProvider.Tests 12 | { 13 | public class MockMemcachedClient : IMemcachedClient 14 | { 15 | private SessionNodeLocator _locator; 16 | 17 | private Dictionary> _data; 18 | 19 | public MockMemcachedClient(IList nodes) 20 | { 21 | _data = new Dictionary>(); 22 | foreach (var node in nodes) 23 | { 24 | _data.Add(node, new Dictionary()); 25 | } 26 | 27 | _locator = new SessionNodeLocator(); 28 | _locator.Initialize(nodes); 29 | } 30 | 31 | public T Get(string key) 32 | { 33 | var node = _locator.Locate(key); 34 | Console.WriteLine("Get => {0}:{1}", key, node == null? "null" : node.EndPoint.ToString()); 35 | 36 | if (node == null 37 | || _data[node] == null 38 | || !_data[node].ContainsKey(key)) 39 | return default(T); 40 | 41 | return (T)_data[node][key]; 42 | } 43 | 44 | public bool Store(StoreMode mode, string key, object value, TimeSpan validFor) 45 | { 46 | var node = _locator.Locate(key); 47 | Console.WriteLine("Store => {0}:{1}", key, node == null ? "null" : node.EndPoint.ToString()); 48 | 49 | _data[node][key] = value; 50 | return true; 51 | } 52 | 53 | public bool Remove(string key) 54 | { 55 | throw new NotImplementedException(); 56 | } 57 | 58 | public void SetNodeDead(IMemcachedNode n1, List activeNodes) 59 | { 60 | Console.WriteLine("Down => {0}", n1.EndPoint); 61 | 62 | // Having to use reflection to set a private field 63 | var prop = n1.GetType().GetField("internalPoolImpl", BindingFlags.NonPublic | BindingFlags.Instance); 64 | var internalPoolImpl = prop.GetValue(n1); 65 | var prop2 = internalPoolImpl.GetType().GetField("isAlive", BindingFlags.NonPublic | BindingFlags.Instance); 66 | prop2.SetValue(internalPoolImpl, false); 67 | 68 | _data[n1] = null; 69 | _locator = new SessionNodeLocator(); 70 | _locator.Initialize(activeNodes); 71 | } 72 | 73 | public void SetNodeAlive(IMemcachedNode n1, List activeNodes) 74 | { 75 | Console.WriteLine("Up => {0}", n1.EndPoint); 76 | 77 | // Having to use reflection to set a private field 78 | var prop = n1.GetType().GetField("internalPoolImpl", BindingFlags.NonPublic | BindingFlags.Instance); 79 | var internalPoolImpl = prop.GetValue(n1); 80 | var prop2 = internalPoolImpl.GetType().GetField("isAlive", BindingFlags.NonPublic | BindingFlags.Instance); 81 | prop2.SetValue(internalPoolImpl, true); 82 | 83 | _data[n1] = new Dictionary(); 84 | _locator.Initialize(activeNodes); 85 | } 86 | 87 | public void Dispose() 88 | { 89 | 90 | } 91 | 92 | #region Not Implemented 93 | 94 | 95 | public object Get(string key) 96 | { 97 | throw new NotImplementedException(); 98 | } 99 | 100 | public IDictionary Get(IEnumerable keys) 101 | { 102 | throw new NotImplementedException(); 103 | } 104 | 105 | public bool TryGet(string key, out object value) 106 | { 107 | throw new NotImplementedException(); 108 | } 109 | 110 | public bool TryGetWithCas(string key, out CasResult value) 111 | { 112 | throw new NotImplementedException(); 113 | } 114 | 115 | public CasResult GetWithCas(string key) 116 | { 117 | throw new NotImplementedException(); 118 | } 119 | 120 | public CasResult GetWithCas(string key) 121 | { 122 | throw new NotImplementedException(); 123 | } 124 | 125 | public IDictionary> GetWithCas(IEnumerable keys) 126 | { 127 | throw new NotImplementedException(); 128 | } 129 | 130 | public bool Append(string key, ArraySegment data) 131 | { 132 | throw new NotImplementedException(); 133 | } 134 | 135 | public CasResult Append(string key, ulong cas, ArraySegment data) 136 | { 137 | throw new NotImplementedException(); 138 | } 139 | 140 | public bool Prepend(string key, ArraySegment data) 141 | { 142 | throw new NotImplementedException(); 143 | } 144 | 145 | public CasResult Prepend(string key, ulong cas, ArraySegment data) 146 | { 147 | throw new NotImplementedException(); 148 | } 149 | 150 | public bool Store(StoreMode mode, string key, object value) 151 | { 152 | throw new NotImplementedException(); 153 | } 154 | 155 | public bool Store(StoreMode mode, string key, object value, DateTime expiresAt) 156 | { 157 | throw new NotImplementedException(); 158 | } 159 | 160 | public CasResult Cas(StoreMode mode, string key, object value) 161 | { 162 | throw new NotImplementedException(); 163 | } 164 | 165 | public CasResult Cas(StoreMode mode, string key, object value, ulong cas) 166 | { 167 | throw new NotImplementedException(); 168 | } 169 | 170 | public CasResult Cas(StoreMode mode, string key, object value, DateTime expiresAt, ulong cas) 171 | { 172 | throw new NotImplementedException(); 173 | } 174 | 175 | public CasResult Cas(StoreMode mode, string key, object value, TimeSpan validFor, ulong cas) 176 | { 177 | throw new NotImplementedException(); 178 | } 179 | 180 | public ulong Decrement(string key, ulong defaultValue, ulong delta) 181 | { 182 | throw new NotImplementedException(); 183 | } 184 | 185 | public ulong Decrement(string key, ulong defaultValue, ulong delta, DateTime expiresAt) 186 | { 187 | throw new NotImplementedException(); 188 | } 189 | 190 | public ulong Decrement(string key, ulong defaultValue, ulong delta, TimeSpan validFor) 191 | { 192 | throw new NotImplementedException(); 193 | } 194 | 195 | public CasResult Decrement(string key, ulong defaultValue, ulong delta, ulong cas) 196 | { 197 | throw new NotImplementedException(); 198 | } 199 | 200 | public CasResult Decrement(string key, ulong defaultValue, ulong delta, DateTime expiresAt, ulong cas) 201 | { 202 | throw new NotImplementedException(); 203 | } 204 | 205 | public CasResult Decrement(string key, ulong defaultValue, ulong delta, TimeSpan validFor, ulong cas) 206 | { 207 | throw new NotImplementedException(); 208 | } 209 | 210 | public ulong Increment(string key, ulong defaultValue, ulong delta) 211 | { 212 | throw new NotImplementedException(); 213 | } 214 | 215 | public ulong Increment(string key, ulong defaultValue, ulong delta, DateTime expiresAt) 216 | { 217 | throw new NotImplementedException(); 218 | } 219 | 220 | public ulong Increment(string key, ulong defaultValue, ulong delta, TimeSpan validFor) 221 | { 222 | throw new NotImplementedException(); 223 | } 224 | 225 | public CasResult Increment(string key, ulong defaultValue, ulong delta, ulong cas) 226 | { 227 | throw new NotImplementedException(); 228 | } 229 | 230 | public CasResult Increment(string key, ulong defaultValue, ulong delta, DateTime expiresAt, ulong cas) 231 | { 232 | throw new NotImplementedException(); 233 | } 234 | 235 | public CasResult Increment(string key, ulong defaultValue, ulong delta, TimeSpan validFor, ulong cas) 236 | { 237 | throw new NotImplementedException(); 238 | } 239 | 240 | public void FlushAll() 241 | { 242 | throw new NotImplementedException(); 243 | } 244 | 245 | public ServerStats Stats() 246 | { 247 | throw new NotImplementedException(); 248 | } 249 | 250 | public ServerStats Stats(string type) 251 | { 252 | throw new NotImplementedException(); 253 | } 254 | 255 | public event Action NodeFailed; 256 | 257 | #endregion 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /MemcachedSessionProvider/SessionNodeLocatorImpl.cs: -------------------------------------------------------------------------------- 1 | #region [License] 2 | /* ************************************************************ 3 | * 4 | * Copyright (c) 2014 Rohit Agarwal 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * ************************************************************/ 19 | #endregion 20 | 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Linq; 24 | using System.Security.Cryptography; 25 | using System.Text; 26 | using System.Threading; 27 | using Enyim.Caching; 28 | using Enyim.Caching.Memcached; 29 | 30 | namespace MemcachedSessionProvider 31 | { 32 | internal class SessionNodeLocatorImpl 33 | { 34 | private const int ServerAddressMutations = 100; 35 | 36 | // holds all server keys for mapping an item key to the server consistently 37 | private uint[] _keys; 38 | private Dictionary _masterKeys; 39 | 40 | // used to lookup a server based on its key 41 | private Dictionary _keyToServer; 42 | private Dictionary _keyToBackup; 43 | private List _allServers; 44 | private ReaderWriterLockSlim _serverAccessLock; 45 | private SessionKeyFormat _sessionKeyFormat; 46 | 47 | public SessionNodeLocatorImpl() 48 | { 49 | _masterKeys = new Dictionary(new UIntEqualityComparer()); 50 | _keyToServer = new Dictionary(new UIntEqualityComparer()); 51 | _keyToBackup = new Dictionary(new UIntEqualityComparer()); 52 | _allServers = new List(); 53 | _serverAccessLock = new ReaderWriterLockSlim(); 54 | _sessionKeyFormat = new SessionKeyFormat(); 55 | } 56 | 57 | public void Initialize(IList nodes) 58 | { 59 | _serverAccessLock.EnterWriteLock(); 60 | 61 | try 62 | { 63 | _allServers = nodes.ToList(); 64 | 65 | if (_keys == null) 66 | { 67 | BuildIndex(); 68 | BuildMasterPrimaryAndBackup(); 69 | } 70 | else 71 | { 72 | ReBuildMaster(); 73 | } 74 | 75 | 76 | } 77 | finally 78 | { 79 | _serverAccessLock.ExitWriteLock(); 80 | } 81 | } 82 | 83 | public IMemcachedNode Locate(string key) 84 | { 85 | if (key == null) throw new ArgumentNullException("key"); 86 | 87 | _serverAccessLock.EnterReadLock(); 88 | 89 | try { return PerformLocate(key); } 90 | finally { _serverAccessLock.ExitReadLock(); } 91 | } 92 | 93 | public IEnumerable GetWorkingNodes() 94 | { 95 | return _allServers; 96 | } 97 | 98 | public bool AssignPrimaryBackupNodes(string key) 99 | { 100 | _serverAccessLock.EnterUpgradeableReadLock(); 101 | 102 | try { return PerformNodeAssignment(key); } 103 | finally { _serverAccessLock.ExitUpgradeableReadLock(); } 104 | } 105 | 106 | private IMemcachedNode PerformLocate(string key) 107 | { 108 | IMemcachedNode node; 109 | uint? itemKeyHash = GetPrimaryKeyHash(key); 110 | if (itemKeyHash == null || _allServers.Count == 0) 111 | { 112 | node = null; 113 | } 114 | else if (_sessionKeyFormat.IsBackupKey(key)) 115 | { 116 | node = _keyToBackup.ContainsKey(itemKeyHash.Value) ? _keyToBackup[itemKeyHash.Value] : null; 117 | } 118 | else // Primary key 119 | { 120 | node = _keyToServer.ContainsKey(itemKeyHash.Value) ? _keyToServer[itemKeyHash.Value] : null; 121 | } 122 | 123 | if (node == null || node.IsAlive) 124 | { 125 | return node; 126 | } 127 | 128 | return null; 129 | } 130 | 131 | private bool PerformNodeAssignment(string key) 132 | { 133 | uint? itemKeyHash = GetPrimaryKeyHash(key); 134 | if (itemKeyHash == null 135 | || _allServers.Count == 0 136 | || !_masterKeys.ContainsKey(itemKeyHash.Value)) 137 | { 138 | return false; 139 | } 140 | 141 | var node = _masterKeys[itemKeyHash.Value]; 142 | var backupNode = FindNextNodeForBackup(node); 143 | 144 | bool locationChanged = NotEqual(node, _keyToServer[itemKeyHash.Value]) 145 | || NotEqual(backupNode, _keyToBackup[itemKeyHash.Value]); 146 | 147 | if (locationChanged) 148 | { 149 | _serverAccessLock.EnterWriteLock(); 150 | _keyToServer[itemKeyHash.Value] = node; 151 | _keyToBackup[itemKeyHash.Value] = backupNode; 152 | _serverAccessLock.ExitWriteLock(); 153 | } 154 | 155 | return locationChanged; 156 | } 157 | 158 | private uint? GetPrimaryKeyHash(string rawKey) 159 | { 160 | if (_keys.Length == 0) return null; 161 | 162 | // clean the key from any backup prefix 163 | string key = _sessionKeyFormat.GetPrimaryKey(rawKey); 164 | 165 | uint itemKeyHash = BitConverter.ToUInt32(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(key)), 0); 166 | // get the index of the server assigned to this hash 167 | int foundIndex = Array.BinarySearch(_keys, itemKeyHash); 168 | 169 | // no exact match 170 | if (foundIndex < 0) 171 | { 172 | // this is the nearest server in the list 173 | foundIndex = ~foundIndex; 174 | 175 | if (foundIndex == 0) 176 | { 177 | // it's smaller than everything, so use the last server (with the highest key) 178 | foundIndex = _keys.Length - 1; 179 | } 180 | else if (foundIndex >= _keys.Length) 181 | { 182 | // the key was larger than all server keys, so return the first server 183 | foundIndex = 0; 184 | } 185 | } 186 | 187 | if (foundIndex < 0 || foundIndex > _keys.Length) 188 | return null; 189 | 190 | return _keys[foundIndex]; 191 | } 192 | 193 | /// 194 | /// Get the next available node for the given one. For the last node 195 | /// the first one is returned. If this list contains only a 196 | /// single node, conceptionally there's no next node, so null 197 | /// is returned. 198 | /// 199 | private IMemcachedNode FindNextNodeForBackup(IMemcachedNode primaryNode) 200 | { 201 | if (primaryNode == null || _allServers.Count == 1) 202 | { 203 | return null; 204 | } 205 | 206 | var idx = _allServers.FindIndex(v => v.EndPoint.Equals(primaryNode.EndPoint)); 207 | var nextIdx = (idx == _allServers.Count - 1) ? 0 : idx + 1; 208 | var backupNode = _allServers[nextIdx]; 209 | if (backupNode.EndPoint.Equals(primaryNode.EndPoint)) 210 | { 211 | backupNode = null; 212 | } 213 | 214 | return backupNode; 215 | } 216 | 217 | private void BuildIndex() 218 | { 219 | var keys = new uint[_allServers.Count * ServerAddressMutations]; 220 | 221 | int nodeIdx = 0; 222 | 223 | foreach (IMemcachedNode node in _allServers) 224 | { 225 | var tmpKeys = GenerateKeys(node, ServerAddressMutations); 226 | tmpKeys.CopyTo(keys, nodeIdx); 227 | nodeIdx += ServerAddressMutations; 228 | } 229 | 230 | Array.Sort(keys); 231 | Interlocked.Exchange(ref _keys, keys); 232 | } 233 | 234 | private static uint[] GenerateKeys(IMemcachedNode node, int numberOfKeys) 235 | { 236 | const int keyLength = 4; 237 | const int partCount = 1; // (ModifiedFNV.HashSize / 8) / KeyLength; // HashSize is in bits, uint is 4 byte long 238 | 239 | var k = new uint[partCount * numberOfKeys]; 240 | 241 | // every server is registered numberOfKeys times 242 | // using UInt32s generated from the different parts of the hash 243 | // i.e. hash is 64 bit: 244 | // 00 00 aa bb 00 00 cc dd 245 | // server will be stored with keys 0x0000aabb & 0x0000ccdd 246 | // (or a bit differently based on the little/big indianness of the host) 247 | string address = node.EndPoint.ToString(); 248 | var md5 = MD5.Create(); 249 | 250 | for (int i = 0; i < numberOfKeys; i++) 251 | { 252 | byte[] data = md5.ComputeHash(Encoding.ASCII.GetBytes(String.Concat(address, "-", i))); 253 | 254 | for (int h = 0; h < partCount; h++) 255 | { 256 | k[i * partCount + h] = BitConverter.ToUInt32(data, h * keyLength); 257 | } 258 | } 259 | 260 | return k; 261 | } 262 | 263 | private void BuildMasterPrimaryAndBackup() 264 | { 265 | _masterKeys.Clear(); 266 | _keyToServer.Clear(); 267 | _keyToBackup.Clear(); 268 | 269 | int numNodes = _allServers.Count; 270 | if (numNodes == 0) 271 | { 272 | return; 273 | } 274 | 275 | int keysPerServer = _keys.Length / numNodes; 276 | for (int i = 0; i < numNodes; i++) 277 | { 278 | int start = i * keysPerServer; 279 | int end = (i == numNodes - 1) ? _keys.Length : start + keysPerServer; 280 | for (int j = start; j < end; j++) 281 | { 282 | _masterKeys[_keys[j]] = _allServers[i]; 283 | _keyToServer[_keys[j]] = _allServers[i]; 284 | _keyToBackup[_keys[j]] = FindNextNodeForBackup(_allServers[i]); 285 | } 286 | } 287 | } 288 | 289 | private void ReBuildMaster() 290 | { 291 | _masterKeys.Clear(); 292 | 293 | int numNodes = _allServers.Count; 294 | if (numNodes == 0) 295 | { 296 | return; 297 | } 298 | 299 | int keysPerServer = _keys.Length / numNodes; 300 | for (int i = 0; i < numNodes; i++) 301 | { 302 | int start = i * keysPerServer; 303 | int end = (i == numNodes - 1) ? _keys.Length : start + keysPerServer; 304 | for (int j = start; j < end; j++) 305 | { 306 | _masterKeys[_keys[j]] = _allServers[i]; 307 | } 308 | } 309 | } 310 | 311 | private bool NotEqual(IMemcachedNode first, IMemcachedNode second) 312 | { 313 | if (first == null && second == null) 314 | { 315 | return false; 316 | } 317 | 318 | if (first == null || second == null) 319 | { 320 | return true; 321 | } 322 | 323 | return !first.EndPoint.Equals(second.EndPoint); 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /MemcachedSessionProvider.Tests/SessionNodeLocatorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Reflection; 5 | using Enyim.Caching.Configuration; 6 | using Enyim.Caching.Memcached; 7 | using NUnit.Framework; 8 | 9 | namespace MemcachedSessionProvider.Tests 10 | { 11 | [TestFixture] 12 | public class SessionNodeLocatorTests 13 | { 14 | private const string N2Key = "__AspSession_abcdefgh_34567890"; 15 | private const string N1Key = "__AspSession_abcdefgh"; 16 | private const string N2Key2Node = "__AspSession_345678906789"; 17 | private const string BackupPrefix = "bak:"; 18 | 19 | ISocketPoolConfiguration s = new SocketPoolConfiguration(); 20 | private SessionNodeLocator locator; 21 | 22 | [SetUp] 23 | public void Setup() 24 | { 25 | locator = new SessionNodeLocator(); 26 | locator.Reset(); 27 | } 28 | 29 | [Test] 30 | public void TestSingleNode() 31 | { 32 | var n1 = GetNode(1); 33 | locator.Initialize(new List {n1}); 34 | 35 | 36 | var primary = locator.Locate(N1Key); 37 | Assert.AreEqual(n1.EndPoint, primary.EndPoint); 38 | 39 | var backup = locator.Locate(BackupPrefix + N1Key); 40 | Assert.IsNull(backup, "No back if single node"); 41 | } 42 | 43 | [Test] 44 | public void TestSingleNodeDown() 45 | { 46 | var n1 = GetNode(1); 47 | locator.Initialize(new List { n1 }); 48 | 49 | SetNodeDead(n1, new List()); 50 | 51 | var primary = locator.Locate(N1Key); 52 | Assert.IsNull(primary); 53 | 54 | var backup = locator.Locate(BackupPrefix + N1Key); 55 | Assert.IsNull(backup); 56 | } 57 | 58 | [Test] 59 | public void TestTwoNodes() 60 | { 61 | var n1 = GetNode(1); 62 | var n2 = GetNode(2); 63 | locator.Initialize(new List { n1, n2 }); 64 | 65 | var primary = locator.Locate(N1Key); 66 | Assert.AreEqual(n1.EndPoint, primary.EndPoint); 67 | 68 | var backup = locator.Locate(BackupPrefix + N1Key); 69 | Assert.AreEqual(n2.EndPoint, backup.EndPoint, "Backup is on next node"); 70 | } 71 | 72 | [Test] 73 | public void TestTwoNodesLoopBack() 74 | { 75 | var n1 = GetNode(1); 76 | var n2 = GetNode(2); 77 | locator.Initialize(new List { n1, n2 }); 78 | 79 | var primary = locator.Locate(N2Key2Node); 80 | Assert.AreEqual(n2.EndPoint, primary.EndPoint); 81 | 82 | var backup = locator.Locate(BackupPrefix + N2Key2Node); 83 | Assert.AreEqual(n1.EndPoint, backup.EndPoint, "Backup loops back to first if primary is on last node"); 84 | } 85 | 86 | [Test] 87 | public void TestTwoNodesWithFirstNodeDown() 88 | { 89 | var n1 = GetNode(1); 90 | var n2 = GetNode(2); 91 | locator.Initialize(new List { n1, n2 }); 92 | 93 | SetNodeDead(n1, new List{n2}); 94 | 95 | var primary = locator.Locate(N1Key); 96 | Assert.IsNull(primary, "Primary Node is down"); 97 | 98 | var backup = locator.Locate(BackupPrefix + N1Key); 99 | Assert.AreEqual(n2.EndPoint, backup.EndPoint, "Backup is available"); 100 | } 101 | 102 | [Test] 103 | public void TestBackupTwoNodesWithFirstNodeDown() 104 | { 105 | var n1 = GetNode(1); 106 | var n2 = GetNode(2); 107 | locator.Initialize(new List { n1, n2 }); 108 | 109 | SetNodeDead(n1, new List { n2 }); 110 | 111 | var backup = locator.Locate(BackupPrefix + N1Key); 112 | Assert.AreEqual(n2.EndPoint, backup.EndPoint); 113 | 114 | // now primary moves 115 | locator.AssignPrimaryBackupNodes(N1Key); 116 | 117 | var primary = locator.Locate(N1Key); 118 | Assert.AreEqual(n2.EndPoint, primary.EndPoint); 119 | 120 | // no backup 121 | backup = locator.Locate(BackupPrefix + N1Key); 122 | Assert.IsNull(backup, "No backup if single node"); 123 | } 124 | 125 | [Test] 126 | public void TestTwoNodesWithBackupNodeDown() 127 | { 128 | var n1 = GetNode(1); 129 | var n2 = GetNode(2); 130 | locator.Initialize(new List { n1, n2 }); 131 | 132 | SetNodeDead(n2, new List{n1}); 133 | 134 | var primary = locator.Locate(N2Key); 135 | Assert.AreEqual(n1.EndPoint, primary.EndPoint); 136 | 137 | var backup = locator.Locate(BackupPrefix + N2Key); 138 | Assert.IsNull(backup, "Don't return dead node"); 139 | } 140 | 141 | [Test] 142 | public void TestTwoNodesWithAllNodesDown() 143 | { 144 | var n1 = GetNode(1); 145 | var n2 = GetNode(2); 146 | locator.Initialize(new List { n1, n2 }); 147 | 148 | SetNodeDead(n1, new List{n2}); 149 | SetNodeDead(n2, new List()); 150 | 151 | var primary = locator.Locate(N2Key); 152 | Assert.IsNull(primary); 153 | 154 | var backup = locator.Locate(BackupPrefix + N2Key); 155 | Assert.IsNull(backup); 156 | } 157 | 158 | [Test] 159 | public void TestThreeNodes() 160 | { 161 | var n1 = GetNode(1); 162 | var n2 = GetNode(2); 163 | var n3 = GetNode(3); 164 | locator.Initialize(new List { n1, n2, n3 }); 165 | 166 | var primary = locator.Locate(N2Key); 167 | Assert.AreEqual(n2.EndPoint, primary.EndPoint); 168 | 169 | var backup = locator.Locate(BackupPrefix + N2Key); 170 | Assert.AreEqual(n3.EndPoint, backup.EndPoint); 171 | } 172 | 173 | [Test] 174 | public void TestThreeNodesWithBackupNodeDown() 175 | { 176 | var n1 = GetNode(1); 177 | var n2 = GetNode(2); 178 | var n3 = GetNode(3); 179 | locator.Initialize(new List { n1, n2, n3 }); 180 | 181 | SetNodeDead(n3, new List{n1, n2}); 182 | 183 | var backup = locator.Locate(BackupPrefix + N2Key2Node); 184 | Assert.IsNull(backup); 185 | 186 | locator.AssignPrimaryBackupNodes(N2Key2Node); 187 | 188 | backup = locator.Locate(BackupPrefix + N2Key2Node); 189 | Assert.AreEqual(n1.EndPoint, backup.EndPoint, "Backup skips dead nodes"); 190 | } 191 | 192 | [Test] 193 | public void TestBackupNodeDoesntMoveAfterNodeFailure() 194 | { 195 | var n1 = GetNode(1); 196 | var n2 = GetNode(2); 197 | var n3 = GetNode(3); 198 | var n4 = GetNode(4); 199 | locator.Initialize(new List { n1, n2, n3, n4 }); 200 | 201 | var backup = locator.Locate(BackupPrefix + N1Key); 202 | Assert.AreEqual(n2.EndPoint, backup.EndPoint, "Backup on next node"); 203 | 204 | SetNodeDead(n1, new List { n2, n3, n4 }); 205 | 206 | backup = locator.Locate(BackupPrefix + N1Key); 207 | Assert.AreEqual(n2.EndPoint, backup.EndPoint, "After primary node failure, don't move the backup node"); 208 | } 209 | 210 | [Test] 211 | public void TestBackupNodeMoveAfterNodeFailure() 212 | { 213 | var n1 = GetNode(1); 214 | var n2 = GetNode(2); 215 | var n3 = GetNode(3); 216 | var n4 = GetNode(4); 217 | locator.Initialize(new List { n1, n2, n3, n4 }); 218 | 219 | SetNodeDead(n1, new List { n2, n3, n4 }); 220 | 221 | var primary = locator.Locate(N1Key); 222 | var backup = locator.Locate(BackupPrefix + N1Key); 223 | Assert.IsNull(primary); 224 | Assert.AreEqual(n2.EndPoint, backup.EndPoint, "After primary node failure, don't move the backup node"); 225 | 226 | locator.AssignPrimaryBackupNodes(N1Key); 227 | primary = locator.Locate(N1Key); 228 | backup = locator.Locate(BackupPrefix + N1Key); 229 | Assert.AreEqual(n2.EndPoint, primary.EndPoint); 230 | Assert.AreEqual(n3.EndPoint, backup.EndPoint, "Backup moves after a call to Assign"); 231 | } 232 | 233 | [Test] 234 | public void TestNodeRecovery() 235 | { 236 | var n1 = GetNode(1); 237 | var n2 = GetNode(2); 238 | var n3 = GetNode(3); 239 | var n4 = GetNode(4); 240 | locator.Initialize(new List { n1, n2, n3, n4 }); 241 | 242 | SetNodeDead(n1, new List { n2, n3, n4 }); 243 | 244 | var primary = locator.Locate(N1Key); 245 | var backup = locator.Locate(BackupPrefix + N1Key); 246 | Assert.IsNull(primary); 247 | Assert.AreEqual(n2.EndPoint, backup.EndPoint, "After primary node failure, don't move the backup node"); 248 | 249 | locator.AssignPrimaryBackupNodes(N1Key); 250 | primary = locator.Locate(N1Key); 251 | backup = locator.Locate(BackupPrefix + N1Key); 252 | Assert.AreEqual(n2.EndPoint, primary.EndPoint); 253 | Assert.AreEqual(n3.EndPoint, backup.EndPoint, "Backup moves after a call to Assign"); 254 | 255 | SetNodeAlive(n1, new List { n1, n2, n3, n4 }); 256 | 257 | // no move yet 258 | primary = locator.Locate(N1Key); 259 | backup = locator.Locate(BackupPrefix + N1Key); 260 | Assert.AreEqual(n2.EndPoint, primary.EndPoint); 261 | Assert.AreEqual(n3.EndPoint, backup.EndPoint); 262 | 263 | // Reassign 264 | locator.AssignPrimaryBackupNodes(N1Key); 265 | primary = locator.Locate(N1Key); 266 | backup = locator.Locate(BackupPrefix + N1Key); 267 | Assert.AreEqual(n1.EndPoint, primary.EndPoint); 268 | Assert.AreEqual(n2.EndPoint, backup.EndPoint); 269 | 270 | } 271 | 272 | [Test] 273 | public void TestNodeWithAppRestart() 274 | { 275 | var n1 = GetNode(1); 276 | var n2 = GetNode(2); 277 | var n3 = GetNode(3); 278 | var n4 = GetNode(4); 279 | locator.Initialize(new List { n1, n2, n3, n4 }); 280 | 281 | var primary = locator.Locate(N1Key); 282 | var backup = locator.Locate(BackupPrefix + N1Key); 283 | Assert.AreEqual(n1.EndPoint, primary.EndPoint); 284 | Assert.AreEqual(n2.EndPoint, backup.EndPoint); 285 | 286 | locator.Reset(); // client app went down 287 | locator.Initialize(new List { n1, n2, n3, n4 }); 288 | primary = locator.Locate(N1Key); 289 | backup = locator.Locate(BackupPrefix + N1Key); 290 | Assert.AreEqual(n1.EndPoint, primary.EndPoint); 291 | Assert.AreEqual(n2.EndPoint, backup.EndPoint); 292 | } 293 | 294 | private IMemcachedNode GetNode(int num) 295 | { 296 | var p1 = new IPEndPoint(IPAddress.Parse(string.Format("10.10.10.{0}", num)), 11211); 297 | return new MemcachedNode(p1, s); 298 | } 299 | 300 | private void SetNodeDead(IMemcachedNode n1, List activeNodes) 301 | { 302 | // Having to use reflection to set a private field 303 | var prop = n1.GetType().GetField("internalPoolImpl", BindingFlags.NonPublic | BindingFlags.Instance); 304 | var internalPoolImpl = prop.GetValue(n1); 305 | var prop2 = internalPoolImpl.GetType().GetField("isAlive", BindingFlags.NonPublic | BindingFlags.Instance); 306 | prop2.SetValue(internalPoolImpl, false); 307 | 308 | locator = new SessionNodeLocator(); 309 | locator.Initialize(activeNodes); 310 | } 311 | 312 | private void SetNodeAlive(IMemcachedNode n1, List activeNodes) 313 | { 314 | // Having to use reflection to set a private field 315 | var prop = n1.GetType().GetField("internalPoolImpl", BindingFlags.NonPublic | BindingFlags.Instance); 316 | var internalPoolImpl = prop.GetValue(n1); 317 | var prop2 = internalPoolImpl.GetType().GetField("isAlive", BindingFlags.NonPublic | BindingFlags.Instance); 318 | prop2.SetValue(internalPoolImpl, true); 319 | 320 | locator.Initialize(activeNodes); 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /MemcachedSessionProvider/SessionProvider.cs: -------------------------------------------------------------------------------- 1 | #region [License] 2 | /* ************************************************************ 3 | * 4 | * Copyright (c) 2014 Rohit Agarwal 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * ************************************************************/ 19 | #endregion 20 | 21 | #region MSDN References 22 | /* 23 | Implementing a Session-State Store Provider, http://msdn.microsoft.com/en-us/library/ms178587.aspx 24 | Sample Session-State Store Provider, http://msdn.microsoft.com/en-us/library/ms178589.aspx 25 | */ 26 | #endregion 27 | 28 | using System; 29 | using System.Configuration; 30 | using System.Web; 31 | using System.Web.Configuration; 32 | using System.Web.SessionState; 33 | 34 | namespace MemcachedSessionProvider 35 | { 36 | public class SessionProvider : SessionStateStoreProviderBase 37 | { 38 | private TimeSpan _sessionTimeout; 39 | private readonly SessionCacheWithBackup _sessionCache = SessionCacheWithBackup.Instance; 40 | 41 | public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) 42 | { 43 | if (config == null) 44 | throw new ArgumentNullException("config"); 45 | 46 | if (String.IsNullOrEmpty(name)) 47 | name = "MemcachedSessionProvider"; 48 | 49 | if (String.IsNullOrEmpty(config["description"])) 50 | { 51 | config.Remove("description"); 52 | config.Add("description", "Memcached Session Provider"); 53 | } 54 | 55 | // Initialize the abstract base class. 56 | base.Initialize(name, config); 57 | 58 | // Get Timeout value 59 | var objConfig = (SessionStateSection)WebConfigurationManager.GetSection("system.web/sessionState"); 60 | _sessionTimeout = objConfig.Timeout; 61 | 62 | _sessionCache.InitializeClient(); 63 | } 64 | 65 | /// 66 | /// Takes as input the HttpContext instance for the current request and performs 67 | /// any initialization required. 68 | /// 69 | public override void InitializeRequest(HttpContext context) 70 | { 71 | // nothing required for our memcached provider 72 | } 73 | 74 | /// 75 | /// Takes as input the HttpContext instance for the current request and performs 76 | /// any cleanup required. 77 | /// 78 | public override void EndRequest(HttpContext context) 79 | { 80 | // nothing required for our memcached provider 81 | } 82 | 83 | /// 84 | /// Frees any resources no longer in use by the session-state store provider. 85 | /// 86 | public override void Dispose() 87 | { 88 | 89 | } 90 | 91 | 92 | /// 93 | /// Takes as input the HttpContext instance for the current request and the 94 | /// SessionID value for the current request. Retrieves session values and information 95 | /// from Memcached. 96 | /// 97 | /// This method is supposed to lock the session-item data at the data store for the duration 98 | /// of the request. But this implementation is "lockless". To keep the performance fast, 99 | /// there is no locking of any session data. 100 | /// 101 | /// The GetItemExclusive method sets several output-parameter values that inform the calling 102 | /// SessionStateModule about the state of the current session-state item in the data store. 103 | /// If no session item data is found at the data store, the GetItemExclusive method sets the 104 | /// locked output parameter to false and returns null. This causes SessionStateModule to call 105 | /// the CreateNewStoreData method to create a new SessionStateStoreData object for the request. 106 | /// 107 | /// If session-item data is found at the data store, the GetItemExclusive method returns the item. 108 | /// The locked output parameter is still set to false, the lockAge output parameter is set to 0, 109 | /// the lockId output parameter is set to DateTime.UtcNow.Ticks. This implementation is not 110 | /// keeping track of lockId. 111 | /// 112 | /// The actionFlags parameter is used with sessions whose Cookieless property is true, when 113 | /// the regenerateExpiredSessionId attribute is set to true. An actionFlags value set to 1 114 | /// (SessionStateActions.InitializeItem) indicates that the entry in the session data store 115 | /// is a new session that requires initialization. Uninitialized entries in the session data store 116 | /// are created by a call to the CreateUninitializedItem method. If the item from the session data 117 | /// store is already initialized, the actionFlags parameter is set to zero (SessionStateActions.None). 118 | /// 119 | /// To support cookieless sessions, the actionFlags output parameter is set to the value returned 120 | /// from the session data store for the current item. If the actionFlags parameter value for the 121 | /// requested session-store item equals the InitializeItem enumeration value (1), the the value in 122 | /// the data store is set to zero after setting the actionFlags out parameter. 123 | /// 124 | public override SessionStateStoreData GetItemExclusive( 125 | HttpContext context, string id, 126 | out bool locked, out TimeSpan lockAge, out object lockId, 127 | out SessionStateActions actions) 128 | { 129 | return GetSessionStoreItem(context, id, out locked, out lockAge, out lockId, out actions); 130 | } 131 | 132 | /// 133 | /// This method performs the same work as the GetItemExclusive method. 134 | /// The GetItem method is called when the EnableSessionState attribute is set to ReadOnly. 135 | /// 136 | public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, 137 | out TimeSpan lockAge, out object lockId, out SessionStateActions actions) 138 | { 139 | return GetSessionStoreItem(context, id, out locked, out lockAge, out lockId, out actions); 140 | } 141 | 142 | private SessionStateStoreData GetSessionStoreItem( 143 | HttpContext context, 144 | string id, 145 | out bool locked, 146 | out TimeSpan lockAge, 147 | out object lockId, 148 | out SessionStateActions actions) 149 | { 150 | locked = false; // There is no locking of any session data 151 | lockAge = TimeSpan.Zero; 152 | lockId = DateTime.UtcNow.Ticks; 153 | actions = SessionStateActions.None; 154 | 155 | var cacheItem = _sessionCache.Get(id); 156 | if (cacheItem == null) 157 | { 158 | return null; 159 | } 160 | 161 | SessionStateStoreData sessionData = cacheItem.Deserialize(context); 162 | actions = cacheItem.GetActionFlag(); 163 | return sessionData; 164 | } 165 | 166 | /// 167 | /// Takes as input the HttpContext instance for the current request, the SessionID 168 | /// value for the current request, a SessionStateStoreData object that contains the 169 | /// current session values to be stored, the lock identifier for the current request, 170 | /// and a value that indicates whether the data to be stored is for a new session or 171 | /// an existing session. 172 | /// 173 | /// For Memcached, it doesn't matter if the newItem parameter is true. This method will 174 | /// update the data store with the supplied values. 175 | /// 176 | /// Note that there is no lock on the data that is to be released. Also, since the lockid 177 | /// is not tracked, there is check to see if session data matches the supplied lock identifier 178 | /// before values are updated. Whichever request calls this method last, will update the 179 | /// session data. 180 | /// 181 | /// After the SetAndReleaseItemExclusive method is called, the ResetItemTimeout method is 182 | /// called by SessionStateModule to update the expiration date and time of the session-item data. 183 | /// 184 | public override void SetAndReleaseItemExclusive( 185 | HttpContext context, 186 | string id, 187 | SessionStateStoreData item, 188 | object lockId, 189 | bool newItem) 190 | { 191 | var cacheItem = new SessionData(SessionStateActions.None, item.Timeout); 192 | cacheItem.Serialize((SessionStateItemCollection)item.Items); 193 | _sessionCache.Store(id, cacheItem, new TimeSpan(0, item.Timeout, 0)); 194 | } 195 | 196 | /// 197 | /// Takes as input the HttpContext instance for the current request, the SessionID value 198 | /// for the current request, and the lock identifier for the current request, and releases 199 | /// the lock on an item in the session data store. This method is called when the GetItem 200 | /// or GetItemExclusive method is called and the data store specifies that the requested 201 | /// item is locked, but the lock age has exceeded the ExecutionTimeout value. The lock is 202 | /// cleared by this method, freeing the item for use by other requests. 203 | /// 204 | public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) 205 | { 206 | //no lock to release 207 | } 208 | 209 | /// 210 | /// Takes as input the HttpContext instance for the current request, the SessionID value 211 | /// for the current request, and the lock identifier for the current request, and deletes 212 | /// the session information from the data store where the data store item matches the supplied 213 | /// SessionID value. This method is called when the Abandon method is called. 214 | /// 215 | public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item) 216 | { 217 | _sessionCache.Remove(id); 218 | } 219 | 220 | /// 221 | /// Takes as input the HttpContext instance for the current request, and the SessionID value 222 | /// for the current request, and adds an uninitialized item to the session data store with 223 | /// an actionFlags value of InitializeItem. 224 | /// 225 | /// The CreateUninitializedItem method is used with cookieless sessions when the 226 | /// regenerateExpiredSessionId attribute is set to true, which causes SessionStateModule to 227 | /// generate a new SessionID value when an expired session ID is encountered. 228 | /// 229 | /// The process of generating a new SessionID value requires the browser to be redirected 230 | /// to a URL that contains the newly generated session ID. The CreateUninitializedItem method 231 | /// is called during an initial request that contains an expired session ID. After 232 | /// SessionStateModule acquires a new SessionID value to replace the expired session ID, it 233 | /// calls the CreateUninitializedItem method to add an uninitialized entry to the session-state 234 | /// data store. The browser is then redirected to the URL containing the newly generated SessionID 235 | /// value. The existence of the uninitialized entry in the session data store ensures that the 236 | /// redirected request with the newly generated SessionID value is not mistaken for a request 237 | /// for an expired session, and instead is treated as a new session. 238 | /// 239 | /// The uninitialized entry in the session data store is associated with the newly generated 240 | /// SessionID value and contains only default values, including an expiration date and time, 241 | /// and a value that corresponds to the actionFlags parameter of the GetItem and GetItemExclusive 242 | /// methods. The uninitialized entry in the session state store should include an actionFlags 243 | /// value equal to the InitializeItem enumeration value (1). This value is passed to 244 | /// SessionStateModule by the GetItem and GetItemExclusive methods and specifies for 245 | /// SessionStateModule that the current session is a new session. SessionStateModule will then 246 | /// initialize the new session and raise the Session_OnStart event. 247 | /// 248 | public override void CreateUninitializedItem(HttpContext context, string id, int timeout) 249 | { 250 | var cacheItem = new SessionData(SessionStateActions.InitializeItem, timeout); 251 | _sessionCache.Store(id, cacheItem, new TimeSpan(0, timeout, 0)); 252 | } 253 | 254 | /// 255 | /// Takes as input the HttpContext instance for the current request and the Timeout value for 256 | /// the current session, and returns a new SessionStateStoreData object with an empty 257 | /// ISessionStateItemCollection object, an HttpStaticObjectsCollection collection, and the 258 | /// specified Timeout value. The HttpStaticObjectsCollection instance for the ASP.NET 259 | /// application can be retrieved using the GetSessionStaticObjects method. 260 | /// 261 | public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) 262 | { 263 | return new SessionStateStoreData( 264 | new SessionStateItemCollection(), 265 | SessionStateUtility.GetSessionStaticObjects(context), 266 | timeout); 267 | } 268 | 269 | /// 270 | /// Takes as input a delegate that references the Session_OnEnd event defined in the 271 | /// Global.asax file. If the session-state store provider supports the Session_OnEnd event, 272 | /// a local reference to the SessionStateItemExpireCallback parameter is set and the method 273 | /// returns true; otherwise, the method returns false. 274 | /// 275 | public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) 276 | { 277 | // not supported. 278 | return false; 279 | } 280 | 281 | public override void ResetItemTimeout(HttpContext context, string id) 282 | { 283 | var obj = _sessionCache.Get(id); 284 | 285 | if (obj != null) 286 | { 287 | _sessionCache.Store(id, obj, _sessionTimeout); 288 | } 289 | } 290 | } 291 | } 292 | --------------------------------------------------------------------------------