diff --git a/AsyncGenerator.yml b/AsyncGenerator.yml index c5118e76..44c62cbd 100644 --- a/AsyncGenerator.yml +++ b/AsyncGenerator.yml @@ -28,6 +28,61 @@ registerPlugin: - type: AsyncGenerator.Core.Plugins.EmptyRegionRemover assemblyName: AsyncGenerator.Core +- filePath: StackExchangeRedis\NHibernate.Caches.StackExchangeRedis\NHibernate.Caches.StackExchangeRedis.csproj + targetFramework: net461 + concurrentRun: true + applyChanges: true + analyzation: + methodConversion: + - conversion: Ignore + hasAttributeName: ObsoleteAttribute + - conversion: Smart + containingTypeName: AbstractRegionStrategy + - conversion: Smart + containingTypeName: RedisKeyLocker + callForwarding: true + cancellationTokens: + guards: true + methodParameter: + - parameter: Required + requiresCancellationToken: + - containingType: NHibernate.Caches.StackExchangeRedis.RedisCache + name: GetMany + - containingType: NHibernate.Caches.StackExchangeRedis.RedisCache + name: PutMany + - containingType: NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy + name: Get + - containingType: NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy + name: GetMany + - containingType: NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy + name: Put + - containingType: NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy + name: PutMany + - containingType: NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy + name: Remove + - containingType: NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy + name: Unlock + - containingType: NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy + name: UnlockMany + - containingType: NHibernate.Caches.StackExchangeRedis.AbstractRegionStrategy + name: Clear + - containingType: NHibernate.Caches.StackExchangeRedis.RedisKeyLocker + name: Unlock + - containingType: NHibernate.Caches.StackExchangeRedis.RedisKeyLocker + name: UnlockMany + scanMethodBody: true + searchAsyncCounterpartsInInheritedTypes: true + scanForMissingAsyncMembers: + - all: true + transformation: + configureAwaitArgument: false + localFunctions: true + asyncLock: + type: NHibernate.Util.AsyncLock + methodName: LockAsync + registerPlugin: + - type: AsyncGenerator.Core.Plugins.EmptyRegionRemover + assemblyName: AsyncGenerator.Core - filePath: NHibernate.Caches.Common.Tests\NHibernate.Caches.Common.Tests.csproj targetFramework: net461 concurrentRun: true @@ -356,6 +411,54 @@ registerPlugin: - type: AsyncGenerator.Core.Plugins.NUnitAsyncCounterpartsFinder assemblyName: AsyncGenerator.Core +- filePath: StackExchangeRedis\NHibernate.Caches.StackExchangeRedis.Tests\NHibernate.Caches.StackExchangeRedis.Tests.csproj + targetFramework: net461 + concurrentRun: true + applyChanges: true + analyzation: + methodConversion: + - conversion: Ignore + hasAttributeName: OneTimeSetUpAttribute + - conversion: Ignore + hasAttributeName: OneTimeTearDownAttribute + - conversion: Ignore + hasAttributeName: SetUpAttribute + - conversion: Ignore + hasAttributeName: TearDownAttribute + - conversion: Smart + hasAttributeName: TestAttribute + - conversion: Smart + hasAttributeName: TheoryAttribute + preserveReturnType: + - hasAttributeName: TestAttribute + - hasAttributeName: TheoryAttribute + typeConversion: + - conversion: Ignore + hasAttributeName: IgnoreAttribute + - conversion: Partial + hasAttributeName: TestFixtureAttribute + - conversion: Partial + anyBaseTypeRule: HasTestFixtureAttribute + ignoreSearchForMethodReferences: + - hasAttributeName: TheoryAttribute + - hasAttributeName: TestAttribute + cancellationTokens: + withoutCancellationToken: + - hasAttributeName: TestAttribute + - hasAttributeName: TheoryAttribute + exceptionHandling: + catchMethodBody: + - all: true + result: false + ignoreSearchForAsyncCounterparts: + - name: Disassemble + scanMethodBody: true + searchAsyncCounterpartsInInheritedTypes: true + scanForMissingAsyncMembers: + - all: true + registerPlugin: + - type: AsyncGenerator.Core.Plugins.NUnitAsyncCounterpartsFinder + assemblyName: AsyncGenerator.Core - filePath: SysCache\NHibernate.Caches.SysCache.Tests\NHibernate.Caches.SysCache.Tests.csproj targetFramework: net461 concurrentRun: true diff --git a/NHibernate.Caches.Common.Tests/Async/CacheFixture.cs b/NHibernate.Caches.Common.Tests/Async/CacheFixture.cs index 2d46633d..49bbe473 100644 --- a/NHibernate.Caches.Common.Tests/Async/CacheFixture.cs +++ b/NHibernate.Caches.Common.Tests/Async/CacheFixture.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using NHibernate.Cache; using NUnit.Framework; @@ -61,6 +62,63 @@ public async Task TestRemoveAsync() Assert.That(item, Is.Null, "item still exists in cache after remove"); } + [Test] + public async Task TestLockUnlockAsync() + { + if (!SupportsLocking) + Assert.Ignore("Test not supported by provider"); + + const string key = "keyTestLock"; + const string value = "valueLock"; + + var cache = GetDefaultCache(); + + // add the item + await (cache.PutAsync(key, value, CancellationToken.None)); + + await (cache.LockAsync(key, CancellationToken.None)); + Assert.ThrowsAsync(() => cache.LockAsync(key, CancellationToken.None)); + + await (Task.Delay(cache.Timeout / Timestamper.OneMs)); + + for (var i = 0; i < 2; i++) + { + var lockValue = await (cache.LockAsync(key, CancellationToken.None)); + await (cache.UnlockAsync(key, lockValue, CancellationToken.None)); + } + } + + [Test] + public async Task TestConcurrentLockUnlockAsync() + { + if (!SupportsLocking) + Assert.Ignore("Test not supported by provider"); + + const string value = "value"; + const string key = "keyToLock"; + + var cache = GetDefaultCache(); + + await (cache.PutAsync(key, value, CancellationToken.None)); + Assert.That(await (cache.GetAsync(key, CancellationToken.None)), Is.EqualTo(value), "Unable to retrieved cached object for key"); + + // Simulate NHibernate ReadWriteCache behavior with multiple concurrent threads + // Thread 1 + var lockValue = await (cache.LockAsync(key, CancellationToken.None)); + // Thread 2 + Assert.ThrowsAsync(() => cache.LockAsync(key, CancellationToken.None), "The key should be locked"); + // Thread 3 + Assert.ThrowsAsync(() => cache.LockAsync(key, CancellationToken.None), "The key should still be locked"); + + // Thread 1 + await (cache.UnlockAsync(key, lockValue, CancellationToken.None)); + + Assert.DoesNotThrowAsync(async () => lockValue = await (cache.LockAsync(key, CancellationToken.None)), "The key should be unlocked"); + await (cache.UnlockAsync(key, lockValue, CancellationToken.None)); + + await (cache.RemoveAsync(key, CancellationToken.None)); + } + [Test] public async Task TestClearAsync() { @@ -134,6 +192,108 @@ public async Task TestRegionsAsync() Assert.That(get2, Is.EqualTo(s2), "Unexpected value in cache2"); } + [Test] + public async Task TestPutManyAsync() + { + var keys = new object[10]; + var values = new object[10]; + for (var i = 0; i < keys.Length; i++) + { + keys[i] = $"keyTestPut{i}"; + values[i] = $"valuePut{i}"; + } + + var cache = GetDefaultCache(); + // Due to async version, it may already be there. + foreach (var key in keys) + await (cache.RemoveAsync(key, CancellationToken.None)); + + Assert.That(await (cache.GetManyAsync(keys, CancellationToken.None)), Is.EquivalentTo(new object[10]), "cache returned items we didn't add !?!"); + + await (cache.PutManyAsync(keys, values, CancellationToken.None)); + var items = await (cache.GetManyAsync(keys, CancellationToken.None)); + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + Assert.That(item, Is.Not.Null, "unable to retrieve cached item"); + Assert.That(item, Is.EqualTo(values[i]), "didn't return the item we added"); + } + } + + [Test] + public async Task TestLockUnlockManyAsync() + { + if (!SupportsLocking) + Assert.Ignore("Test not supported by provider"); + + var keys = new object[10]; + var values = new object[10]; + for (var i = 0; i < keys.Length; i++) + { + keys[i] = $"keyTestLock{i}"; + values[i] = $"valueLock{i}"; + } + + var cache = GetDefaultCache(); + + // add the item + await (cache.PutManyAsync(keys, values, CancellationToken.None)); + await (cache.LockManyAsync(keys, CancellationToken.None)); + Assert.ThrowsAsync(() => cache.LockManyAsync(keys, CancellationToken.None), "all items should be locked"); + + await (Task.Delay(cache.Timeout / Timestamper.OneMs)); + + for (var i = 0; i < 2; i++) + { + Assert.DoesNotThrowAsync(async () => + { + await (cache.UnlockManyAsync(keys, await (cache.LockManyAsync(keys, CancellationToken.None)), CancellationToken.None)); + }, "the items should be unlocked"); + } + + // Test partial locks by locking the first 5 keys and afterwards try to lock last 6 keys. + var lockValue = await (cache.LockManyAsync(keys.Take(5).ToArray(), CancellationToken.None)); + + Assert.ThrowsAsync(() => cache.LockManyAsync(keys.Skip(4).ToArray(), CancellationToken.None), "the fifth key should be locked"); + + Assert.DoesNotThrowAsync(async () => + { + await (cache.UnlockManyAsync(keys, await (cache.LockManyAsync(keys.Skip(5).ToArray(), CancellationToken.None)), CancellationToken.None)); + }, "the last 5 keys should not be locked."); + + // Unlock the first 5 keys + await (cache.UnlockManyAsync(keys, lockValue, CancellationToken.None)); + + Assert.DoesNotThrowAsync(async () => + { + lockValue = await (cache.LockManyAsync(keys, CancellationToken.None)); + await (cache.UnlockManyAsync(keys, lockValue, CancellationToken.None)); + }, "the first 5 keys should not be locked."); + } + + [Test] + public void TestNullKeyPutManyAsync() + { + var cache = GetDefaultCache(); + Assert.ThrowsAsync(() => cache.PutManyAsync(null, null, CancellationToken.None)); + } + + [Test] + public void TestNullValuePutManyAsync() + { + var cache = GetDefaultCache(); + Assert.ThrowsAsync(() => cache.PutManyAsync(new object[] { "keyTestNullValuePut" }, null, CancellationToken.None)); + } + + [Test] + public async Task TestNullKeyGetManyAsync() + { + var cache = GetDefaultCache(); + await (cache.PutAsync("keyTestNullKeyGet", "value", CancellationToken.None)); + Assert.ThrowsAsync(() => cache.GetManyAsync(null, CancellationToken.None)); + } + [Test] public async Task TestNonEqualObjectsWithEqualHashCodeAndToStringAsync() { diff --git a/NHibernate.Caches.Common.Tests/CacheFixture.cs b/NHibernate.Caches.Common.Tests/CacheFixture.cs index 577cedd4..f48d0ec5 100644 --- a/NHibernate.Caches.Common.Tests/CacheFixture.cs +++ b/NHibernate.Caches.Common.Tests/CacheFixture.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using NHibernate.Cache; using NUnit.Framework; @@ -10,6 +11,7 @@ namespace NHibernate.Caches.Common.Tests public abstract partial class CacheFixture : Fixture { protected virtual bool SupportsSlidingExpiration => false; + protected virtual bool SupportsLocking => false; protected virtual bool SupportsDistinguishingKeysWithSameStringRepresentationAndHashcode => true; protected virtual bool SupportsClear => true; @@ -54,6 +56,63 @@ public void TestRemove() Assert.That(item, Is.Null, "item still exists in cache after remove"); } + [Test] + public void TestLockUnlock() + { + if (!SupportsLocking) + Assert.Ignore("Test not supported by provider"); + + const string key = "keyTestLock"; + const string value = "valueLock"; + + var cache = GetDefaultCache(); + + // add the item + cache.Put(key, value); + + cache.Lock(key); + Assert.Throws(() => cache.Lock(key)); + + Thread.Sleep(cache.Timeout / Timestamper.OneMs); + + for (var i = 0; i < 2; i++) + { + var lockValue = cache.Lock(key); + cache.Unlock(key, lockValue); + } + } + + [Test] + public void TestConcurrentLockUnlock() + { + if (!SupportsLocking) + Assert.Ignore("Test not supported by provider"); + + const string value = "value"; + const string key = "keyToLock"; + + var cache = GetDefaultCache(); + + cache.Put(key, value); + Assert.That(cache.Get(key), Is.EqualTo(value), "Unable to retrieved cached object for key"); + + // Simulate NHibernate ReadWriteCache behavior with multiple concurrent threads + // Thread 1 + var lockValue = cache.Lock(key); + // Thread 2 + Assert.Throws(() => cache.Lock(key), "The key should be locked"); + // Thread 3 + Assert.Throws(() => cache.Lock(key), "The key should still be locked"); + + // Thread 1 + cache.Unlock(key, lockValue); + + Assert.DoesNotThrow(() => lockValue = cache.Lock(key), "The key should be unlocked"); + cache.Unlock(key, lockValue); + + cache.Remove(key); + } + [Test] public void TestClear() { @@ -134,6 +193,108 @@ public void TestRegions() Assert.That(get2, Is.EqualTo(s2), "Unexpected value in cache2"); } + [Test] + public void TestPutMany() + { + var keys = new object[10]; + var values = new object[10]; + for (var i = 0; i < keys.Length; i++) + { + keys[i] = $"keyTestPut{i}"; + values[i] = $"valuePut{i}"; + } + + var cache = GetDefaultCache(); + // Due to async version, it may already be there. + foreach (var key in keys) + cache.Remove(key); + + Assert.That(cache.GetMany(keys), Is.EquivalentTo(new object[10]), "cache returned items we didn't add !?!"); + + cache.PutMany(keys, values); + var items = cache.GetMany(keys); + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + Assert.That(item, Is.Not.Null, "unable to retrieve cached item"); + Assert.That(item, Is.EqualTo(values[i]), "didn't return the item we added"); + } + } + + [Test] + public void TestLockUnlockMany() + { + if (!SupportsLocking) + Assert.Ignore("Test not supported by provider"); + + var keys = new object[10]; + var values = new object[10]; + for (var i = 0; i < keys.Length; i++) + { + keys[i] = $"keyTestLock{i}"; + values[i] = $"valueLock{i}"; + } + + var cache = GetDefaultCache(); + + // add the item + cache.PutMany(keys, values); + cache.LockMany(keys); + Assert.Throws(() => cache.LockMany(keys), "all items should be locked"); + + Thread.Sleep(cache.Timeout / Timestamper.OneMs); + + for (var i = 0; i < 2; i++) + { + Assert.DoesNotThrow(() => + { + cache.UnlockMany(keys, cache.LockMany(keys)); + }, "the items should be unlocked"); + } + + // Test partial locks by locking the first 5 keys and afterwards try to lock last 6 keys. + var lockValue = cache.LockMany(keys.Take(5).ToArray()); + + Assert.Throws(() => cache.LockMany(keys.Skip(4).ToArray()), "the fifth key should be locked"); + + Assert.DoesNotThrow(() => + { + cache.UnlockMany(keys, cache.LockMany(keys.Skip(5).ToArray())); + }, "the last 5 keys should not be locked."); + + // Unlock the first 5 keys + cache.UnlockMany(keys, lockValue); + + Assert.DoesNotThrow(() => + { + lockValue = cache.LockMany(keys); + cache.UnlockMany(keys, lockValue); + }, "the first 5 keys should not be locked."); + } + + [Test] + public void TestNullKeyPutMany() + { + var cache = GetDefaultCache(); + Assert.Throws(() => cache.PutMany(null, null)); + } + + [Test] + public void TestNullValuePutMany() + { + var cache = GetDefaultCache(); + Assert.Throws(() => cache.PutMany(new object[] { "keyTestNullValuePut" }, null)); + } + + [Test] + public void TestNullKeyGetMany() + { + var cache = GetDefaultCache(); + cache.Put("keyTestNullKeyGet", "value"); + Assert.Throws(() => cache.GetMany(null)); + } + [Serializable] protected class SomeObject { diff --git a/NHibernate.Caches.Everything.sln b/NHibernate.Caches.Everything.sln index 7448ffcb..c419c1b6 100644 --- a/NHibernate.Caches.Everything.sln +++ b/NHibernate.Caches.Everything.sln @@ -1,229 +1,243 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2002 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{9BC335BB-4F31-44B4-9C2D-5A97B4742675}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{55271617-8CB8-4225-B338-069033160497}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{132CB859-841B-48C6-AA58-41BE77727E0D}" - ProjectSection(SolutionItems) = preProject - buildcommon.xml = buildcommon.xml - NHibernate.Caches.snk = NHibernate.Caches.snk - readme.md = readme.md - NHibernate.Caches.props = NHibernate.Caches.props - default.build = default.build - AsyncGenerator.yml = AsyncGenerator.yml - appveyor.yml = appveyor.yml - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SysCache", "SysCache\NHibernate.Caches.SysCache\NHibernate.Caches.SysCache.csproj", "{2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SysCache2", "SysCache2\NHibernate.Caches.SysCache2\NHibernate.Caches.SysCache2.csproj", "{F5852ECD-C05B-4C98-B300-702BA8D8244C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Prevalence", "Prevalence\NHibernate.Caches.Prevalence\NHibernate.Caches.Prevalence.csproj", "{6008B514-E6E4-4485-A188-2D3FE802EC95}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.MemCache", "MemCache\NHibernate.Caches.MemCache\NHibernate.Caches.MemCache.csproj", "{A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Velocity", "Velocity\NHibernate.Caches.Velocity\NHibernate.Caches.Velocity.csproj", "{09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SysCache.Tests", "SysCache\NHibernate.Caches.SysCache.Tests\NHibernate.Caches.SysCache.Tests.csproj", "{1EA6533B-EDC0-4585-927B-65A8A607826A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.MemCache.Tests", "MemCache\NHibernate.Caches.MemCache.Tests\NHibernate.Caches.MemCache.Tests.csproj", "{650E352A-EAE4-4D63-9A89-D7DD7FA4630F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SharedCache", "SharedCache\NHibernate.Caches.SharedCache\NHibernate.Caches.SharedCache.csproj", "{F67ECF65-16D8-400D-A3D7-93A359C26A76}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SharedCache.Tests", "SharedCache\NHibernate.Caches.SharedCache.Tests\NHibernate.Caches.SharedCache.Tests.csproj", "{D54BB1AC-274D-4BD7-B89D-05A61078C83F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Prevalence.Tests", "Prevalence\NHibernate.Caches.Prevalence.Tests\NHibernate.Caches.Prevalence.Tests.csproj", "{07372137-0714-485A-AB07-0A0A872D3444}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Velocity.Tests", "Velocity\NHibernate.Caches.Velocity.Tests\NHibernate.Caches.Velocity.Tests.csproj", "{CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.EnyimMemcached", "EnyimMemcached\NHibernate.Caches.EnyimMemcached\NHibernate.Caches.EnyimMemcached.csproj", "{8B477613-1694-4467-A351-E43349D14527}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.EnyimMemcached.Tests", "EnyimMemcached\NHibernate.Caches.EnyimMemcached.Tests\NHibernate.Caches.EnyimMemcached.Tests.csproj", "{49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SysCache2.Tests", "SysCache2\NHibernate.Caches.SysCache2.Tests\NHibernate.Caches.SysCache2.Tests.csproj", "{F56E6488-DE96-4D1A-9913-D36CBC16E945}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.RtMemoryCache", "RtMemoryCache\NHibernate.Caches.RtMemoryCache\NHibernate.Caches.RtMemoryCache.csproj", "{C0295AB5-00FB-4B45-82F6-2060080D01F9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.RtMemoryCache.Tests", "RtMemoryCache\NHibernate.Caches.RtMemoryCache.Tests\NHibernate.Caches.RtMemoryCache.Tests.csproj", "{0B67ADE8-929E-46D2-A4E6-C0D621378EAE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Common.Tests", "NHibernate.Caches.Common.Tests\NHibernate.Caches.Common.Tests.csproj", "{D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreMemoryCache", "CoreMemoryCache\NHibernate.Caches.CoreMemoryCache\NHibernate.Caches.CoreMemoryCache.csproj", "{FD759770-E662-4822-A627-85FAA2B3177C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreMemoryCache.Tests", "CoreMemoryCache\NHibernate.Caches.CoreMemoryCache.Tests\NHibernate.Caches.CoreMemoryCache.Tests.csproj", "{4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.csproj", "{EFB25D54-38E6-441C-9287-4D69E40B1595}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Tests", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Tests\NHibernate.Caches.CoreDistributedCache.Tests.csproj", "{AD359D7F-6E65-48CB-A59A-B78ED58C1309}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Redis", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Redis\NHibernate.Caches.CoreDistributedCache.Redis.csproj", "{0A6D9315-094E-4C6D-8A87-8C642A2D25D7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Memory", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Memory\NHibernate.Caches.CoreDistributedCache.Memory.csproj", "{63AD02AD-1F35-4341-A4C7-7EAEA238F08C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.SqlServer", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.SqlServer\NHibernate.Caches.CoreDistributedCache.SqlServer.csproj", "{03A80D4B-6C72-4F31-8680-DD6119E34CDF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Memcached", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Memcached\NHibernate.Caches.CoreDistributedCache.Memcached.csproj", "{CB7BEB99-1964-4D83-86AD-100439E04186}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Common", "NHibernate.Caches.Common\NHibernate.Caches.Common.csproj", "{15C72C05-4976-4401-91CB-31EF769AADD7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Util.JsonSerializer", "Util\NHibernate.Caches.Util.JsonSerializer\NHibernate.Caches.Util.JsonSerializer.csproj", "{2327C330-0CE6-4F58-96A3-013BEFDFCCEB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Util.JsonSerializer.Tests", "Util\NHibernate.Caches.Util.JsonSerializer.Tests\NHibernate.Caches.Util.JsonSerializer.Tests.csproj", "{26EAA903-63F7-41B1-9197-5944CEBF00C2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}.Release|Any CPU.Build.0 = Release|Any CPU - {F5852ECD-C05B-4C98-B300-702BA8D8244C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F5852ECD-C05B-4C98-B300-702BA8D8244C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F5852ECD-C05B-4C98-B300-702BA8D8244C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F5852ECD-C05B-4C98-B300-702BA8D8244C}.Release|Any CPU.Build.0 = Release|Any CPU - {6008B514-E6E4-4485-A188-2D3FE802EC95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6008B514-E6E4-4485-A188-2D3FE802EC95}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6008B514-E6E4-4485-A188-2D3FE802EC95}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6008B514-E6E4-4485-A188-2D3FE802EC95}.Release|Any CPU.Build.0 = Release|Any CPU - {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}.Release|Any CPU.Build.0 = Release|Any CPU - {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}.Release|Any CPU.Build.0 = Release|Any CPU - {1EA6533B-EDC0-4585-927B-65A8A607826A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1EA6533B-EDC0-4585-927B-65A8A607826A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1EA6533B-EDC0-4585-927B-65A8A607826A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1EA6533B-EDC0-4585-927B-65A8A607826A}.Release|Any CPU.Build.0 = Release|Any CPU - {650E352A-EAE4-4D63-9A89-D7DD7FA4630F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {650E352A-EAE4-4D63-9A89-D7DD7FA4630F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {650E352A-EAE4-4D63-9A89-D7DD7FA4630F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {650E352A-EAE4-4D63-9A89-D7DD7FA4630F}.Release|Any CPU.Build.0 = Release|Any CPU - {F67ECF65-16D8-400D-A3D7-93A359C26A76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F67ECF65-16D8-400D-A3D7-93A359C26A76}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F67ECF65-16D8-400D-A3D7-93A359C26A76}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F67ECF65-16D8-400D-A3D7-93A359C26A76}.Release|Any CPU.Build.0 = Release|Any CPU - {D54BB1AC-274D-4BD7-B89D-05A61078C83F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D54BB1AC-274D-4BD7-B89D-05A61078C83F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D54BB1AC-274D-4BD7-B89D-05A61078C83F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D54BB1AC-274D-4BD7-B89D-05A61078C83F}.Release|Any CPU.Build.0 = Release|Any CPU - {07372137-0714-485A-AB07-0A0A872D3444}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07372137-0714-485A-AB07-0A0A872D3444}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07372137-0714-485A-AB07-0A0A872D3444}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07372137-0714-485A-AB07-0A0A872D3444}.Release|Any CPU.Build.0 = Release|Any CPU - {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}.Release|Any CPU.Build.0 = Release|Any CPU - {8B477613-1694-4467-A351-E43349D14527}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8B477613-1694-4467-A351-E43349D14527}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B477613-1694-4467-A351-E43349D14527}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8B477613-1694-4467-A351-E43349D14527}.Release|Any CPU.Build.0 = Release|Any CPU - {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}.Debug|Any CPU.Build.0 = Debug|Any CPU - {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}.Release|Any CPU.ActiveCfg = Release|Any CPU - {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}.Release|Any CPU.Build.0 = Release|Any CPU - {F56E6488-DE96-4D1A-9913-D36CBC16E945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F56E6488-DE96-4D1A-9913-D36CBC16E945}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F56E6488-DE96-4D1A-9913-D36CBC16E945}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F56E6488-DE96-4D1A-9913-D36CBC16E945}.Release|Any CPU.Build.0 = Release|Any CPU - {C0295AB5-00FB-4B45-82F6-2060080D01F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C0295AB5-00FB-4B45-82F6-2060080D01F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C0295AB5-00FB-4B45-82F6-2060080D01F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C0295AB5-00FB-4B45-82F6-2060080D01F9}.Release|Any CPU.Build.0 = Release|Any CPU - {0B67ADE8-929E-46D2-A4E6-C0D621378EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B67ADE8-929E-46D2-A4E6-C0D621378EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B67ADE8-929E-46D2-A4E6-C0D621378EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B67ADE8-929E-46D2-A4E6-C0D621378EAE}.Release|Any CPU.Build.0 = Release|Any CPU - {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}.Release|Any CPU.Build.0 = Release|Any CPU - {FD759770-E662-4822-A627-85FAA2B3177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD759770-E662-4822-A627-85FAA2B3177C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD759770-E662-4822-A627-85FAA2B3177C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD759770-E662-4822-A627-85FAA2B3177C}.Release|Any CPU.Build.0 = Release|Any CPU - {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Release|Any CPU.Build.0 = Release|Any CPU - {EFB25D54-38E6-441C-9287-4D69E40B1595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EFB25D54-38E6-441C-9287-4D69E40B1595}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EFB25D54-38E6-441C-9287-4D69E40B1595}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EFB25D54-38E6-441C-9287-4D69E40B1595}.Release|Any CPU.Build.0 = Release|Any CPU - {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Release|Any CPU.Build.0 = Release|Any CPU - {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Release|Any CPU.Build.0 = Release|Any CPU - {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Release|Any CPU.Build.0 = Release|Any CPU - {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Release|Any CPU.Build.0 = Release|Any CPU - {CB7BEB99-1964-4D83-86AD-100439E04186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB7BEB99-1964-4D83-86AD-100439E04186}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB7BEB99-1964-4D83-86AD-100439E04186}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB7BEB99-1964-4D83-86AD-100439E04186}.Release|Any CPU.Build.0 = Release|Any CPU - {15C72C05-4976-4401-91CB-31EF769AADD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {15C72C05-4976-4401-91CB-31EF769AADD7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {15C72C05-4976-4401-91CB-31EF769AADD7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15C72C05-4976-4401-91CB-31EF769AADD7}.Release|Any CPU.Build.0 = Release|Any CPU - {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Release|Any CPU.Build.0 = Release|Any CPU - {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {F5852ECD-C05B-4C98-B300-702BA8D8244C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {6008B514-E6E4-4485-A188-2D3FE802EC95} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {1EA6533B-EDC0-4585-927B-65A8A607826A} = {55271617-8CB8-4225-B338-069033160497} - {650E352A-EAE4-4D63-9A89-D7DD7FA4630F} = {55271617-8CB8-4225-B338-069033160497} - {F67ECF65-16D8-400D-A3D7-93A359C26A76} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {D54BB1AC-274D-4BD7-B89D-05A61078C83F} = {55271617-8CB8-4225-B338-069033160497} - {07372137-0714-485A-AB07-0A0A872D3444} = {55271617-8CB8-4225-B338-069033160497} - {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E} = {55271617-8CB8-4225-B338-069033160497} - {8B477613-1694-4467-A351-E43349D14527} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319} = {55271617-8CB8-4225-B338-069033160497} - {F56E6488-DE96-4D1A-9913-D36CBC16E945} = {55271617-8CB8-4225-B338-069033160497} - {C0295AB5-00FB-4B45-82F6-2060080D01F9} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {0B67ADE8-929E-46D2-A4E6-C0D621378EAE} = {55271617-8CB8-4225-B338-069033160497} - {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC} = {55271617-8CB8-4225-B338-069033160497} - {FD759770-E662-4822-A627-85FAA2B3177C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119} = {55271617-8CB8-4225-B338-069033160497} - {EFB25D54-38E6-441C-9287-4D69E40B1595} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {AD359D7F-6E65-48CB-A59A-B78ED58C1309} = {55271617-8CB8-4225-B338-069033160497} - {0A6D9315-094E-4C6D-8A87-8C642A2D25D7} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {63AD02AD-1F35-4341-A4C7-7EAEA238F08C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {03A80D4B-6C72-4F31-8680-DD6119E34CDF} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {CB7BEB99-1964-4D83-86AD-100439E04186} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {15C72C05-4976-4401-91CB-31EF769AADD7} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {2327C330-0CE6-4F58-96A3-013BEFDFCCEB} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} - {26EAA903-63F7-41B1-9197-5944CEBF00C2} = {55271617-8CB8-4225-B338-069033160497} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2002 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{9BC335BB-4F31-44B4-9C2D-5A97B4742675}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{55271617-8CB8-4225-B338-069033160497}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{132CB859-841B-48C6-AA58-41BE77727E0D}" + ProjectSection(SolutionItems) = preProject + buildcommon.xml = buildcommon.xml + NHibernate.Caches.snk = NHibernate.Caches.snk + readme.md = readme.md + NHibernate.Caches.props = NHibernate.Caches.props + default.build = default.build + AsyncGenerator.yml = AsyncGenerator.yml + appveyor.yml = appveyor.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SysCache", "SysCache\NHibernate.Caches.SysCache\NHibernate.Caches.SysCache.csproj", "{2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SysCache2", "SysCache2\NHibernate.Caches.SysCache2\NHibernate.Caches.SysCache2.csproj", "{F5852ECD-C05B-4C98-B300-702BA8D8244C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Prevalence", "Prevalence\NHibernate.Caches.Prevalence\NHibernate.Caches.Prevalence.csproj", "{6008B514-E6E4-4485-A188-2D3FE802EC95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.MemCache", "MemCache\NHibernate.Caches.MemCache\NHibernate.Caches.MemCache.csproj", "{A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Velocity", "Velocity\NHibernate.Caches.Velocity\NHibernate.Caches.Velocity.csproj", "{09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SysCache.Tests", "SysCache\NHibernate.Caches.SysCache.Tests\NHibernate.Caches.SysCache.Tests.csproj", "{1EA6533B-EDC0-4585-927B-65A8A607826A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.MemCache.Tests", "MemCache\NHibernate.Caches.MemCache.Tests\NHibernate.Caches.MemCache.Tests.csproj", "{650E352A-EAE4-4D63-9A89-D7DD7FA4630F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SharedCache", "SharedCache\NHibernate.Caches.SharedCache\NHibernate.Caches.SharedCache.csproj", "{F67ECF65-16D8-400D-A3D7-93A359C26A76}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SharedCache.Tests", "SharedCache\NHibernate.Caches.SharedCache.Tests\NHibernate.Caches.SharedCache.Tests.csproj", "{D54BB1AC-274D-4BD7-B89D-05A61078C83F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Prevalence.Tests", "Prevalence\NHibernate.Caches.Prevalence.Tests\NHibernate.Caches.Prevalence.Tests.csproj", "{07372137-0714-485A-AB07-0A0A872D3444}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Velocity.Tests", "Velocity\NHibernate.Caches.Velocity.Tests\NHibernate.Caches.Velocity.Tests.csproj", "{CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.EnyimMemcached", "EnyimMemcached\NHibernate.Caches.EnyimMemcached\NHibernate.Caches.EnyimMemcached.csproj", "{8B477613-1694-4467-A351-E43349D14527}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.EnyimMemcached.Tests", "EnyimMemcached\NHibernate.Caches.EnyimMemcached.Tests\NHibernate.Caches.EnyimMemcached.Tests.csproj", "{49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.SysCache2.Tests", "SysCache2\NHibernate.Caches.SysCache2.Tests\NHibernate.Caches.SysCache2.Tests.csproj", "{F56E6488-DE96-4D1A-9913-D36CBC16E945}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.RtMemoryCache", "RtMemoryCache\NHibernate.Caches.RtMemoryCache\NHibernate.Caches.RtMemoryCache.csproj", "{C0295AB5-00FB-4B45-82F6-2060080D01F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.RtMemoryCache.Tests", "RtMemoryCache\NHibernate.Caches.RtMemoryCache.Tests\NHibernate.Caches.RtMemoryCache.Tests.csproj", "{0B67ADE8-929E-46D2-A4E6-C0D621378EAE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Common.Tests", "NHibernate.Caches.Common.Tests\NHibernate.Caches.Common.Tests.csproj", "{D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreMemoryCache", "CoreMemoryCache\NHibernate.Caches.CoreMemoryCache\NHibernate.Caches.CoreMemoryCache.csproj", "{FD759770-E662-4822-A627-85FAA2B3177C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreMemoryCache.Tests", "CoreMemoryCache\NHibernate.Caches.CoreMemoryCache.Tests\NHibernate.Caches.CoreMemoryCache.Tests.csproj", "{4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.csproj", "{EFB25D54-38E6-441C-9287-4D69E40B1595}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Tests", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Tests\NHibernate.Caches.CoreDistributedCache.Tests.csproj", "{AD359D7F-6E65-48CB-A59A-B78ED58C1309}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Redis", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Redis\NHibernate.Caches.CoreDistributedCache.Redis.csproj", "{0A6D9315-094E-4C6D-8A87-8C642A2D25D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Memory", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Memory\NHibernate.Caches.CoreDistributedCache.Memory.csproj", "{63AD02AD-1F35-4341-A4C7-7EAEA238F08C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.SqlServer", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.SqlServer\NHibernate.Caches.CoreDistributedCache.SqlServer.csproj", "{03A80D4B-6C72-4F31-8680-DD6119E34CDF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Memcached", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Memcached\NHibernate.Caches.CoreDistributedCache.Memcached.csproj", "{CB7BEB99-1964-4D83-86AD-100439E04186}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Common", "NHibernate.Caches.Common\NHibernate.Caches.Common.csproj", "{15C72C05-4976-4401-91CB-31EF769AADD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Util.JsonSerializer", "Util\NHibernate.Caches.Util.JsonSerializer\NHibernate.Caches.Util.JsonSerializer.csproj", "{2327C330-0CE6-4F58-96A3-013BEFDFCCEB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Util.JsonSerializer.Tests", "Util\NHibernate.Caches.Util.JsonSerializer.Tests\NHibernate.Caches.Util.JsonSerializer.Tests.csproj", "{26EAA903-63F7-41B1-9197-5944CEBF00C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.StackExchangeRedis", "StackExchangeRedis\NHibernate.Caches.StackExchangeRedis\NHibernate.Caches.StackExchangeRedis.csproj", "{726AA9C7-0EA4-47C0-B65D-D42CD3ECEF82}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.StackExchangeRedis.Tests", "StackExchangeRedis\NHibernate.Caches.StackExchangeRedis.Tests\NHibernate.Caches.StackExchangeRedis.Tests.csproj", "{CC2A7851-B1CD-49F7-ACD4-08604C2ACC63}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F}.Release|Any CPU.Build.0 = Release|Any CPU + {F5852ECD-C05B-4C98-B300-702BA8D8244C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5852ECD-C05B-4C98-B300-702BA8D8244C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5852ECD-C05B-4C98-B300-702BA8D8244C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5852ECD-C05B-4C98-B300-702BA8D8244C}.Release|Any CPU.Build.0 = Release|Any CPU + {6008B514-E6E4-4485-A188-2D3FE802EC95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6008B514-E6E4-4485-A188-2D3FE802EC95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6008B514-E6E4-4485-A188-2D3FE802EC95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6008B514-E6E4-4485-A188-2D3FE802EC95}.Release|Any CPU.Build.0 = Release|Any CPU + {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1}.Release|Any CPU.Build.0 = Release|Any CPU + {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8}.Release|Any CPU.Build.0 = Release|Any CPU + {1EA6533B-EDC0-4585-927B-65A8A607826A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EA6533B-EDC0-4585-927B-65A8A607826A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EA6533B-EDC0-4585-927B-65A8A607826A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EA6533B-EDC0-4585-927B-65A8A607826A}.Release|Any CPU.Build.0 = Release|Any CPU + {650E352A-EAE4-4D63-9A89-D7DD7FA4630F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {650E352A-EAE4-4D63-9A89-D7DD7FA4630F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {650E352A-EAE4-4D63-9A89-D7DD7FA4630F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {650E352A-EAE4-4D63-9A89-D7DD7FA4630F}.Release|Any CPU.Build.0 = Release|Any CPU + {F67ECF65-16D8-400D-A3D7-93A359C26A76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F67ECF65-16D8-400D-A3D7-93A359C26A76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F67ECF65-16D8-400D-A3D7-93A359C26A76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F67ECF65-16D8-400D-A3D7-93A359C26A76}.Release|Any CPU.Build.0 = Release|Any CPU + {D54BB1AC-274D-4BD7-B89D-05A61078C83F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D54BB1AC-274D-4BD7-B89D-05A61078C83F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D54BB1AC-274D-4BD7-B89D-05A61078C83F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D54BB1AC-274D-4BD7-B89D-05A61078C83F}.Release|Any CPU.Build.0 = Release|Any CPU + {07372137-0714-485A-AB07-0A0A872D3444}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07372137-0714-485A-AB07-0A0A872D3444}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07372137-0714-485A-AB07-0A0A872D3444}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07372137-0714-485A-AB07-0A0A872D3444}.Release|Any CPU.Build.0 = Release|Any CPU + {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E}.Release|Any CPU.Build.0 = Release|Any CPU + {8B477613-1694-4467-A351-E43349D14527}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B477613-1694-4467-A351-E43349D14527}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B477613-1694-4467-A351-E43349D14527}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B477613-1694-4467-A351-E43349D14527}.Release|Any CPU.Build.0 = Release|Any CPU + {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319}.Release|Any CPU.Build.0 = Release|Any CPU + {F56E6488-DE96-4D1A-9913-D36CBC16E945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F56E6488-DE96-4D1A-9913-D36CBC16E945}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F56E6488-DE96-4D1A-9913-D36CBC16E945}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F56E6488-DE96-4D1A-9913-D36CBC16E945}.Release|Any CPU.Build.0 = Release|Any CPU + {C0295AB5-00FB-4B45-82F6-2060080D01F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0295AB5-00FB-4B45-82F6-2060080D01F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0295AB5-00FB-4B45-82F6-2060080D01F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0295AB5-00FB-4B45-82F6-2060080D01F9}.Release|Any CPU.Build.0 = Release|Any CPU + {0B67ADE8-929E-46D2-A4E6-C0D621378EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B67ADE8-929E-46D2-A4E6-C0D621378EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B67ADE8-929E-46D2-A4E6-C0D621378EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B67ADE8-929E-46D2-A4E6-C0D621378EAE}.Release|Any CPU.Build.0 = Release|Any CPU + {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC}.Release|Any CPU.Build.0 = Release|Any CPU + {FD759770-E662-4822-A627-85FAA2B3177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD759770-E662-4822-A627-85FAA2B3177C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD759770-E662-4822-A627-85FAA2B3177C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD759770-E662-4822-A627-85FAA2B3177C}.Release|Any CPU.Build.0 = Release|Any CPU + {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Release|Any CPU.Build.0 = Release|Any CPU + {EFB25D54-38E6-441C-9287-4D69E40B1595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFB25D54-38E6-441C-9287-4D69E40B1595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFB25D54-38E6-441C-9287-4D69E40B1595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFB25D54-38E6-441C-9287-4D69E40B1595}.Release|Any CPU.Build.0 = Release|Any CPU + {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Release|Any CPU.Build.0 = Release|Any CPU + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Release|Any CPU.Build.0 = Release|Any CPU + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Release|Any CPU.Build.0 = Release|Any CPU + {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Release|Any CPU.Build.0 = Release|Any CPU + {CB7BEB99-1964-4D83-86AD-100439E04186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB7BEB99-1964-4D83-86AD-100439E04186}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB7BEB99-1964-4D83-86AD-100439E04186}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB7BEB99-1964-4D83-86AD-100439E04186}.Release|Any CPU.Build.0 = Release|Any CPU + {15C72C05-4976-4401-91CB-31EF769AADD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15C72C05-4976-4401-91CB-31EF769AADD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15C72C05-4976-4401-91CB-31EF769AADD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15C72C05-4976-4401-91CB-31EF769AADD7}.Release|Any CPU.Build.0 = Release|Any CPU + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Release|Any CPU.Build.0 = Release|Any CPU + {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Release|Any CPU.Build.0 = Release|Any CPU + {726AA9C7-0EA4-47C0-B65D-D42CD3ECEF82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {726AA9C7-0EA4-47C0-B65D-D42CD3ECEF82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {726AA9C7-0EA4-47C0-B65D-D42CD3ECEF82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {726AA9C7-0EA4-47C0-B65D-D42CD3ECEF82}.Release|Any CPU.Build.0 = Release|Any CPU + {CC2A7851-B1CD-49F7-ACD4-08604C2ACC63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC2A7851-B1CD-49F7-ACD4-08604C2ACC63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC2A7851-B1CD-49F7-ACD4-08604C2ACC63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC2A7851-B1CD-49F7-ACD4-08604C2ACC63}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {2D48853D-7F90-4E1F-BE98-035CAB2D4F1F} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {F5852ECD-C05B-4C98-B300-702BA8D8244C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {6008B514-E6E4-4485-A188-2D3FE802EC95} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {A824DAC6-D317-4DBD-B08A-1CA8C3CFB7C1} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {09EA5F04-1F0C-4D5E-A393-2B7B2FD279E8} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {1EA6533B-EDC0-4585-927B-65A8A607826A} = {55271617-8CB8-4225-B338-069033160497} + {650E352A-EAE4-4D63-9A89-D7DD7FA4630F} = {55271617-8CB8-4225-B338-069033160497} + {F67ECF65-16D8-400D-A3D7-93A359C26A76} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {D54BB1AC-274D-4BD7-B89D-05A61078C83F} = {55271617-8CB8-4225-B338-069033160497} + {07372137-0714-485A-AB07-0A0A872D3444} = {55271617-8CB8-4225-B338-069033160497} + {CA4F0A59-844B-4717-8F3B-6F53DC0F0B2E} = {55271617-8CB8-4225-B338-069033160497} + {8B477613-1694-4467-A351-E43349D14527} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {49F3BCBF-924F-4CFD-A5F7-4C8C4F8FC319} = {55271617-8CB8-4225-B338-069033160497} + {F56E6488-DE96-4D1A-9913-D36CBC16E945} = {55271617-8CB8-4225-B338-069033160497} + {C0295AB5-00FB-4B45-82F6-2060080D01F9} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {0B67ADE8-929E-46D2-A4E6-C0D621378EAE} = {55271617-8CB8-4225-B338-069033160497} + {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC} = {55271617-8CB8-4225-B338-069033160497} + {FD759770-E662-4822-A627-85FAA2B3177C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119} = {55271617-8CB8-4225-B338-069033160497} + {EFB25D54-38E6-441C-9287-4D69E40B1595} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {AD359D7F-6E65-48CB-A59A-B78ED58C1309} = {55271617-8CB8-4225-B338-069033160497} + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {03A80D4B-6C72-4F31-8680-DD6119E34CDF} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {CB7BEB99-1964-4D83-86AD-100439E04186} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {15C72C05-4976-4401-91CB-31EF769AADD7} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {26EAA903-63F7-41B1-9197-5944CEBF00C2} = {55271617-8CB8-4225-B338-069033160497} + {726AA9C7-0EA4-47C0-B65D-D42CD3ECEF82} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {CC2A7851-B1CD-49F7-ACD4-08604C2ACC63} = {55271617-8CB8-4225-B338-069033160497} + EndGlobalSection +EndGlobal diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/App.config b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/App.config new file mode 100644 index 00000000..6d036d1b --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/App.config @@ -0,0 +1,44 @@ + + + +
+
+
+ + + + + + + + + + NHibernate.Connection.DriverConnectionProvider, NHibernate + NHibernate.Dialect.MsSql2000Dialect + NHibernate.Driver.SqlClientDriver + + Server=localhost;initial catalog=nhibernate;Integrated Security=SSPI + + ReadCommitted + NHibernate.Caches.StackExchangeRedis.RedisCacheProvider, NHibernate.Caches.StackExchangeRedis + + + + + + + + + + + + + + + + + + + + + diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/Caches/DistributedRedisCache.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/Caches/DistributedRedisCache.cs new file mode 100644 index 00000000..e487dfe1 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/Caches/DistributedRedisCache.cs @@ -0,0 +1,146 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; + +namespace NHibernate.Caches.StackExchangeRedis.Tests.Caches +{ + using System.Threading.Tasks; + using System.Threading; + public partial class DistributedRedisCache : CacheBase + { + + /// + public override Task GetAsync(object key, CancellationToken cancellationToken) + { + // Use a random strategy to get the value. + // A real distributed cache should use a proper load balancing. + var strategy = _regionStrategies[_random.Next(0, _regionStrategies.Length - 1)]; + return strategy.GetAsync(key, cancellationToken); + } + + /// + public override async Task PutAsync(object key, object value, CancellationToken cancellationToken) + { + foreach (var strategy in _regionStrategies) + { + await (strategy.PutAsync(key, value, cancellationToken)); + } + } + + /// + public override async Task PutManyAsync(object[] keys, object[] values, CancellationToken cancellationToken) + { + foreach (var strategy in _regionStrategies) + { + await (strategy.PutManyAsync(keys, values, cancellationToken)); + } + } + + /// + public override async Task RemoveAsync(object key, CancellationToken cancellationToken) + { + foreach (var strategy in _regionStrategies) + { + await (strategy.RemoveAsync(key, cancellationToken)); + } + } + + /// + public override async Task ClearAsync(CancellationToken cancellationToken) + { + foreach (var strategy in _regionStrategies) + { + await (strategy.ClearAsync(cancellationToken)); + } + } + + /// + public override async Task LockAsync(object key, CancellationToken cancellationToken) + { + // A simple locking that requires all instances to obtain the lock + // A real distributed cache should use something like the Redlock algorithm. + var lockValues = new string[_regionStrategies.Length]; + try + { + for (var i = 0; i < _regionStrategies.Length; i++) + { + lockValues[i] = await (_regionStrategies[i].LockAsync(key, cancellationToken)); + } + + return lockValues; + } + catch (CacheException) + { + for (var i = 0; i < _regionStrategies.Length; i++) + { + if (lockValues[i] == null) + { + continue; + } + await (_regionStrategies[i].UnlockAsync(key, lockValues[i], cancellationToken)); + } + throw; + } + } + + /// + public override async Task LockManyAsync(object[] keys, CancellationToken cancellationToken) + { + // A simple locking that requires all instances to obtain the lock + // A real distributed cache should use something like the Redlock algorithm. + var lockValues = new string[_regionStrategies.Length]; + try + { + for (var i = 0; i < _regionStrategies.Length; i++) + { + lockValues[i] = await (_regionStrategies[i].LockManyAsync(keys, cancellationToken)); + } + + return lockValues; + } + catch (CacheException) + { + for (var i = 0; i < _regionStrategies.Length; i++) + { + if (lockValues[i] == null) + { + continue; + } + await (_regionStrategies[i].UnlockManyAsync(keys, lockValues[i], cancellationToken)); + } + throw; + } + } + + /// + public override async Task UnlockAsync(object key, object lockValue, CancellationToken cancellationToken) + { + var lockValues = (string[]) lockValue; + for (var i = 0; i < _regionStrategies.Length; i++) + { + await (_regionStrategies[i].UnlockAsync(key, lockValues[i], cancellationToken)); + } + } + + /// + public override async Task UnlockManyAsync(object[] keys, object lockValue, CancellationToken cancellationToken) + { + var lockValues = (string[]) lockValue; + for (var i = 0; i < _regionStrategies.Length; i++) + { + await (_regionStrategies[i].UnlockManyAsync(keys, lockValues[i], cancellationToken)); + } + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCacheDefaultStrategyFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCacheDefaultStrategyFixture.cs new file mode 100644 index 00000000..3a094cca --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCacheDefaultStrategyFixture.cs @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Threading; +using NHibernate.Cache; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + using System.Threading.Tasks; + public partial class RedisCacheDefaultStrategyFixture : RedisCacheFixture + { + + [Test] + public async Task TestMaxAllowedVersionAsync() + { + var cache = (RedisCache) GetDefaultCache(); + var strategy = (DefaultRegionStrategy)cache.RegionStrategy; + var version = strategy.CurrentVersion; + + var props = GetDefaultProperties(); + props.Add("cache.region_strategy.default.max_allowed_version", version.ToString()); + cache = (RedisCache) DefaultProvider.BuildCache(DefaultRegion, props); + strategy = (DefaultRegionStrategy) cache.RegionStrategy; + + await (cache.ClearAsync(CancellationToken.None)); + + Assert.That(strategy.CurrentVersion, Is.EqualTo(1L), "the version was not reset to 1"); + } + + [Test] + public async Task TestClearWithMultipleClientsAndPubSubAsync() + { + const string key = "keyTestClear"; + const string value = "valueClear"; + + var cache = (RedisCache)GetDefaultCache(); + var strategy = (DefaultRegionStrategy)cache.RegionStrategy; + var cache2 = (RedisCache) GetDefaultCache(); + var strategy2 = (DefaultRegionStrategy) cache2.RegionStrategy; + + // add the item + await (cache.PutAsync(key, value, CancellationToken.None)); + + // make sure it's there + var item = await (cache.GetAsync(key, CancellationToken.None)); + Assert.That(item, Is.Not.Null, "couldn't find item in cache"); + + item = await (cache2.GetAsync(key, CancellationToken.None)); + Assert.That(item, Is.Not.Null, "couldn't find item in second cache"); + + var version = strategy.CurrentVersion; + + // clear the cache + await (cache.ClearAsync(CancellationToken.None)); + + Assert.That(strategy.CurrentVersion, Is.EqualTo(version + 1), "the version has not been updated"); + await (Task.Delay(TimeSpan.FromSeconds(2))); + Assert.That(strategy2.CurrentVersion, Is.EqualTo(version + 1), "the version should be updated with the pub/sub api"); + + // make sure we don't get an item + item = await (cache.GetAsync(key, CancellationToken.None)); + Assert.That(item, Is.Null, "item still exists in cache after clear"); + + item = await (cache2.GetAsync(key, CancellationToken.None)); + Assert.That(item, Is.Null, "item still exists in the second cache after clear"); + } + + [Test] + public async Task TestClearWithMultipleClientsAndNoPubSubAsync() + { + const string key = "keyTestClear"; + const string value = "valueClear"; + + var props = GetDefaultProperties(); + props.Add("cache.region_strategy.default.use_pubsub", "false"); + + var cache = (RedisCache) DefaultProvider.BuildCache(DefaultRegion, props); + var strategy = (DefaultRegionStrategy) cache.RegionStrategy; + var cache2 = (RedisCache) DefaultProvider.BuildCache(DefaultRegion, props); + var strategy2 = (DefaultRegionStrategy) cache2.RegionStrategy; + + // add the item + await (cache.PutAsync(key, value, CancellationToken.None)); + + // make sure it's there + var item = await (cache.GetAsync(key, CancellationToken.None)); + Assert.That(item, Is.Not.Null, "couldn't find item in cache"); + + item = await (cache2.GetAsync(key, CancellationToken.None)); + Assert.That(item, Is.Not.Null, "couldn't find item in second cache"); + + var version = strategy.CurrentVersion; + + // clear the cache + await (cache.ClearAsync(CancellationToken.None)); + + Assert.That(strategy.CurrentVersion, Is.EqualTo(version + 1), "the version has not been updated"); + await (Task.Delay(TimeSpan.FromSeconds(2))); + Assert.That(strategy2.CurrentVersion, Is.EqualTo(version), "the version should not be updated"); + + // make sure we don't get an item + item = await (cache.GetAsync(key, CancellationToken.None)); + Assert.That(item, Is.Null, "item still exists in cache after clear"); + + item = await (cache2.GetAsync(key, CancellationToken.None)); + Assert.That(item, Is.Null, "item still exists in the second cache after clear"); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCacheFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCacheFixture.cs new file mode 100644 index 00000000..89dcb6a3 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCacheFixture.cs @@ -0,0 +1,155 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Xml; +using System.Xml.Linq; +using NHibernate.Cache; +using NHibernate.Cache.Entry; +using NHibernate.Caches.Common.Tests; +using NHibernate.Collection; +using NHibernate.Engine; +using NHibernate.Persister.Entity; +using NHibernate.Type; +using NSubstitute; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + using System.Threading.Tasks; + public abstract partial class RedisCacheFixture : CacheFixture + { + + [Test] + public async Task TestEqualObjectsWithDifferentHashCodeAsync() + { + var value = "value"; + var obj1 = new CustomCacheKey(1, "test", false); + var obj2 = new CustomCacheKey(1, "test", false); + + var cache = GetDefaultCache(); + + await (cache.PutAsync(obj1, value, CancellationToken.None)); + Assert.That(await (cache.GetAsync(obj1, CancellationToken.None)), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(await (cache.GetAsync(obj2, CancellationToken.None)), Is.EqualTo(value), "Unable to retrieved cached object for key obj2"); + await (cache.RemoveAsync(obj1, CancellationToken.None)); + } + + [Test] + public async Task TestEqualObjectsWithDifferentHashCodeAndUseHashCodeGlobalConfigurationAsync() + { + var value = "value"; + var obj1 = new CustomCacheKey(1, "test", false); + var obj2 = new CustomCacheKey(1, "test", false); + + var props = GetDefaultProperties(); + var cacheProvider = ProviderBuilder(); + props[RedisEnvironment.AppendHashcode] = "true"; + cacheProvider.Start(props); + var cache = cacheProvider.BuildCache(DefaultRegion, props); + + await (cache.PutAsync(obj1, value, CancellationToken.None)); + Assert.That(await (cache.GetAsync(obj1, CancellationToken.None)), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(await (cache.GetAsync(obj2, CancellationToken.None)), Is.Null, "The hash code should be used in the cache key"); + await (cache.RemoveAsync(obj1, CancellationToken.None)); + } + + [Test] + public async Task TestEqualObjectsWithDifferentHashCodeAndUseHashCodeRegionConfigurationAsync() + { + var value = "value"; + var obj1 = new CustomCacheKey(1, "test", false); + var obj2 = new CustomCacheKey(1, "test", false); + + var props = GetDefaultProperties(); + var cacheProvider = ProviderBuilder(); + cacheProvider.Start(props); + props["append-hashcode"] = "true"; + var cache = cacheProvider.BuildCache(DefaultRegion, props); + + await (cache.PutAsync(obj1, value, CancellationToken.None)); + Assert.That(await (cache.GetAsync(obj1, CancellationToken.None)), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(await (cache.GetAsync(obj2, CancellationToken.None)), Is.Null, "The hash code should be used in the cache key"); + await (cache.RemoveAsync(obj1, CancellationToken.None)); + } + + [Test] + public async Task TestNonEqualObjectsWithEqualToStringAsync() + { + var value = "value"; + var obj1 = new CustomCacheKey(new ObjectEqualToString(1), "test", true); + var obj2 = new CustomCacheKey(new ObjectEqualToString(2), "test", true); + + var cache = GetDefaultCache(); + + await (cache.PutAsync(obj1, value, CancellationToken.None)); + Assert.That(await (cache.GetAsync(obj1, CancellationToken.None)), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(await (cache.GetAsync(obj2, CancellationToken.None)), Is.EqualTo(value), "Unable to retrieved cached object for key obj2"); + await (cache.RemoveAsync(obj1, CancellationToken.None)); + } + + [Test] + public async Task TestNonEqualObjectsWithEqualToStringUseHashCodeAsync() + { + var value = "value"; + var obj1 = new CustomCacheKey(new ObjectEqualToString(1), "test", true); + var obj2 = new CustomCacheKey(new ObjectEqualToString(2), "test", true); + + var props = GetDefaultProperties(); + var cacheProvider = ProviderBuilder(); + props[RedisEnvironment.AppendHashcode] = "true"; + cacheProvider.Start(props); + var cache = cacheProvider.BuildCache(DefaultRegion, props); + + await (cache.PutAsync(obj1, value, CancellationToken.None)); + Assert.That(await (cache.GetAsync(obj1, CancellationToken.None)), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(await (cache.GetAsync(obj2, CancellationToken.None)), Is.Null, "Unexpectedly found a cache entry for key obj2 after obj1 put"); + await (cache.RemoveAsync(obj1, CancellationToken.None)); + } + + [Test] + public async Task TestEnvironmentNameAsync() + { + var props = GetDefaultProperties(); + + var developProvider = ProviderBuilder(); + props[RedisEnvironment.EnvironmentName] = "develop"; + developProvider.Start(props); + var developCache = developProvider.BuildCache(DefaultRegion, props); + + var releaseProvider = ProviderBuilder(); + props[RedisEnvironment.EnvironmentName] = "release"; + releaseProvider.Start(props); + var releaseCache = releaseProvider.BuildCache(DefaultRegion, props); + + const string key = "testKey"; + const string value = "testValue"; + + await (developCache.PutAsync(key, value, CancellationToken.None)); + + Assert.That(await (releaseCache.GetAsync(key, CancellationToken.None)), Is.Null, "release environment should be separate from develop"); + + await (developCache.RemoveAsync(key, CancellationToken.None)); + await (releaseCache.PutAsync(key, value, CancellationToken.None)); + + Assert.That(await (developCache.GetAsync(key, CancellationToken.None)), Is.Null, "develop environment should be separate from release"); + + await (releaseCache.RemoveAsync(key, CancellationToken.None)); + + developProvider.Stop(); + releaseProvider.Stop(); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCachePerformanceFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCachePerformanceFixture.cs new file mode 100644 index 00000000..ac438770 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Async/RedisCachePerformanceFixture.cs @@ -0,0 +1,120 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using NHibernate.Cache; +using NHibernate.Caches.Common.Tests; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + using System.Threading; + public partial class RedisCachePerformanceFixture : Fixture + { + + [Test] + public Task TestGetOperationAsync() + { + return TestOperationAsync("Get", true, (cache, key, _) => cache.GetAsync(key, CancellationToken.None)); + } + + [Test] + public Task TestGetManyOperationAsync() + { + return TestBatchOperationAsync("GetMany", true, (cache, keys, _) => cache.GetManyAsync(keys, CancellationToken.None), BatchSize); + } + + [Test] + public Task TestGetOperationWithSlidingExpirationAsync() + { + var props = new Dictionary {{"sliding", "true"}}; + return TestOperationAsync("Get", true, (cache, key, _) => cache.GetAsync(key, CancellationToken.None), + caches: new List {GetDefaultRedisCache(props), GetFastRedisCache(props)}); + } + + [Test] + public Task TestGetManyOperationWithSlidingExpirationAsync() + { + var props = new Dictionary {{"sliding", "true"}}; + return TestBatchOperationAsync("GetMany", true, (cache, keys, _) => cache.GetManyAsync(keys, CancellationToken.None), + batchSize: BatchSize, + caches: new List {GetDefaultRedisCache(props), GetFastRedisCache(props)}); + } + + [Test] + public Task TestPutOperationAsync() + { + var props = new Dictionary {{"expiration", "0"}}; + return TestOperationAsync("Put", false, (cache, key, value) => cache.PutAsync(key, value, CancellationToken.None), + caches: new List {GetFastRedisCache(props)}); + } + + [Test] + public Task TestPutManyOperationAsync() + { + var props = new Dictionary {{"expiration", "0"}}; + return TestBatchOperationAsync("PutMany", false, (cache, keys, values) => cache.PutManyAsync(keys, values, CancellationToken.None), + batchSize: null, + caches: new List {GetFastRedisCache(props)}); + } + + [Test] + public Task TestPutOperationWithExpirationAsync() + { + return TestOperationAsync("Put", false, (cache, key, value) => cache.PutAsync(key, value, CancellationToken.None)); + } + + [Test] + public Task TestPutManyOperationWithExpirationAsync() + { + return TestBatchOperationAsync("PutMany", false, (cache, keys, values) => cache.PutManyAsync(keys, values, CancellationToken.None), null); + } + + [Test] + public Task TestLockUnlockOperationAsync() + { + return TestOperationAsync("Lock/Unlock", true, async (cache, key, _) => + { + var value = await (cache.LockAsync(key, CancellationToken.None)); + await (cache.UnlockAsync(key, value, CancellationToken.None)); + }); + } + + [Test] + public Task TestLockUnlockManyOperationAsync() + { + return TestBatchOperationAsync("LockMany/UnlockMany", true, async (cache, keys, _) => + { + var value = await (cache.LockManyAsync(keys, CancellationToken.None)); + await (cache.UnlockManyAsync(keys, value, CancellationToken.None)); + }, null); + } + + private async Task PutCacheDataAsync(CacheBase cache, Dictionary> cacheData, CancellationToken cancellationToken = default(CancellationToken)) + { + foreach (var pair in cacheData) + { + await (cache.PutAsync(pair.Key, pair.Value, cancellationToken)); + } + } + + private async Task RemoveCacheDataAsync(CacheBase cache, Dictionary> cacheData, CancellationToken cancellationToken = default(CancellationToken)) + { + foreach (var pair in cacheData) + { + await (cache.RemoveAsync(pair.Key, cancellationToken)); + } + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Caches/DistributedRedisCache.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Caches/DistributedRedisCache.cs new file mode 100644 index 00000000..e5226450 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Caches/DistributedRedisCache.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; + +namespace NHibernate.Caches.StackExchangeRedis.Tests.Caches +{ + /// + /// Operates with multiple independent Redis instances. This cache should not be used in a real environment + /// as its purpose is just to demonstrate that can be extended for a distributed environment. + /// + public partial class DistributedRedisCache : CacheBase + { + private readonly AbstractRegionStrategy[] _regionStrategies; + private readonly Random _random = new Random(); + + public DistributedRedisCache(RedisCacheRegionConfiguration configuration, IEnumerable regionStrategies) + { + _regionStrategies = regionStrategies.ToArray(); + RegionName = configuration.RegionName; + Timeout = Timestamper.OneMs * (int) configuration.LockConfiguration.KeyTimeout.TotalMilliseconds; + } + + /// + /// The region strategies used by the cache. + /// + public IEnumerable RegionStrategies => _regionStrategies; + + /// + public override int Timeout { get; } + + /// + public override string RegionName { get; } + + /// + public override long NextTimestamp() => Timestamper.Next(); + + /// + public override object Get(object key) + { + // Use a random strategy to get the value. + // A real distributed cache should use a proper load balancing. + var strategy = _regionStrategies[_random.Next(0, _regionStrategies.Length - 1)]; + return strategy.Get(key); + } + + /// + public override void Put(object key, object value) + { + foreach (var strategy in _regionStrategies) + { + strategy.Put(key, value); + } + } + + /// + public override void PutMany(object[] keys, object[] values) + { + foreach (var strategy in _regionStrategies) + { + strategy.PutMany(keys, values); + } + } + + /// + public override void Remove(object key) + { + foreach (var strategy in _regionStrategies) + { + strategy.Remove(key); + } + } + + /// + public override void Clear() + { + foreach (var strategy in _regionStrategies) + { + strategy.Clear(); + } + } + + /// + public override void Destroy() + { + } + + /// + public override object Lock(object key) + { + // A simple locking that requires all instances to obtain the lock + // A real distributed cache should use something like the Redlock algorithm. + var lockValues = new string[_regionStrategies.Length]; + try + { + for (var i = 0; i < _regionStrategies.Length; i++) + { + lockValues[i] = _regionStrategies[i].Lock(key); + } + + return lockValues; + } + catch (CacheException) + { + for (var i = 0; i < _regionStrategies.Length; i++) + { + if (lockValues[i] == null) + { + continue; + } + _regionStrategies[i].Unlock(key, lockValues[i]); + } + throw; + } + } + + /// + public override object LockMany(object[] keys) + { + // A simple locking that requires all instances to obtain the lock + // A real distributed cache should use something like the Redlock algorithm. + var lockValues = new string[_regionStrategies.Length]; + try + { + for (var i = 0; i < _regionStrategies.Length; i++) + { + lockValues[i] = _regionStrategies[i].LockMany(keys); + } + + return lockValues; + } + catch (CacheException) + { + for (var i = 0; i < _regionStrategies.Length; i++) + { + if (lockValues[i] == null) + { + continue; + } + _regionStrategies[i].UnlockMany(keys, lockValues[i]); + } + throw; + } + } + + /// + public override void Unlock(object key, object lockValue) + { + var lockValues = (string[]) lockValue; + for (var i = 0; i < _regionStrategies.Length; i++) + { + _regionStrategies[i].Unlock(key, lockValues[i]); + } + } + + /// + public override void UnlockMany(object[] keys, object lockValue) + { + var lockValues = (string[]) lockValue; + for (var i = 0; i < _regionStrategies.Length; i++) + { + _regionStrategies[i].UnlockMany(keys, lockValues[i]); + } + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/CustomObjectsFactory.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/CustomObjectsFactory.cs new file mode 100644 index 00000000..8b092834 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/CustomObjectsFactory.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using NHibernate.Bytecode; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + public class CustomObjectsFactory : IObjectsFactory + { + private readonly Dictionary _registeredTypes = new Dictionary(); + private readonly Dictionary _registeredSingletons = new Dictionary(); + + public void Register() + { + _registeredTypes.Add(typeof(TBaseType), typeof(TConcreteType)); + } + + public void RegisterSingleton(TBaseType value) + { + _registeredSingletons.Add(typeof(TBaseType), value); + } + + public object CreateInstance(System.Type type) + { + return _registeredSingletons.TryGetValue(type, out var singleton) + ? singleton + : Activator.CreateInstance(_registeredTypes.TryGetValue(type, out var concreteType) ? concreteType : type); + } + + public object CreateInstance(System.Type type, bool nonPublic) + { + throw new NotSupportedException(); + } + + public object CreateInstance(System.Type type, params object[] ctorArgs) + { + throw new NotSupportedException(); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/DistributedRedisCacheFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/DistributedRedisCacheFixture.cs new file mode 100644 index 00000000..8250954a --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/DistributedRedisCacheFixture.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using NHibernate.Cache; +using NHibernate.Caches.Common.Tests; +using NHibernate.Caches.StackExchangeRedis.Tests.Providers; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + [TestFixture] + public class DistributedRedisCacheFixture : CacheFixture + { + protected override bool SupportsSlidingExpiration => true; + protected override bool SupportsLocking => true; + protected override bool SupportsDistinguishingKeysWithSameStringRepresentationAndHashcode => false; + + protected override Func ProviderBuilder => + () => new DistributedRedisCacheProvider(); + + protected override void Configure(Dictionary defaultProperties) + { + // Simulate Redis instances by using databases as instances + defaultProperties.Add(RedisEnvironment.Configuration, "127.0.0.1,defaultDatabase=0;127.0.0.1,defaultDatabase=1"); + base.Configure(defaultProperties); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/NHibernate.Caches.StackExchangeRedis.Tests.csproj b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/NHibernate.Caches.StackExchangeRedis.Tests.csproj new file mode 100644 index 00000000..1ce12581 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/NHibernate.Caches.StackExchangeRedis.Tests.csproj @@ -0,0 +1,29 @@ + + + + NHibernate.Caches.StackExchangeRedis.Tests + Unit tests of cache provider NHibernate using StackExchange.Redis. + net461;netcoreapp2.0 + true + + + NETFX;$(DefineConstants) + + + Exe + false + + + + + + + + + + + + + + + diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Program.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Program.cs new file mode 100644 index 00000000..be90c629 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Program.cs @@ -0,0 +1,12 @@ +#if !NETFX +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + public class Program + { + public static int Main(string[] args) + { + return new NUnitLite.AutoRun(typeof(Program).Assembly).Execute(args); + } + } +} +#endif diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Properties/AssemblyInfo.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..83cc064e --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System; + +[assembly: CLSCompliant(false)] \ No newline at end of file diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Providers/DistributedRedisCacheProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Providers/DistributedRedisCacheProvider.cs new file mode 100644 index 00000000..a8a985b7 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/Providers/DistributedRedisCacheProvider.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using NHibernate.Cache; +using NHibernate.Caches.StackExchangeRedis.Tests.Caches; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis.Tests.Providers +{ + /// + /// Provider for building a cache capable of operating with multiple independent Redis instances. This provider + /// should not be used in a real environment as its purpose is just to demonstrate that + /// can be extended for a distributed environment. + /// + public class DistributedRedisCacheProvider : RedisCacheProvider + { + private readonly List _connectionMultiplexers = new List(); + + /// + protected override void Start(string configurationString, IDictionary properties) + { + foreach (var instanceConfiguration in configurationString.Split(';')) + { + var connectionMultiplexer = CacheConfiguration.ConnectionMultiplexerProvider.Get(instanceConfiguration); + _connectionMultiplexers.Add(connectionMultiplexer); + } + } + + /// + protected override CacheBase BuildCache(RedisCacheRegionConfiguration regionConfiguration, IDictionary properties) + { + var strategies = new List(); + foreach (var connectionMultiplexer in _connectionMultiplexers) + { + var regionStrategy = + CacheConfiguration.RegionStrategyFactory.Create(connectionMultiplexer, regionConfiguration, properties); + regionStrategy.Validate(); + strategies.Add(regionStrategy); + } + return new DistributedRedisCache(regionConfiguration, strategies); + } + + /// + public override void Stop() + { + foreach (var connectionMultiplexer in _connectionMultiplexers) + { + connectionMultiplexer.Dispose(); + } + _connectionMultiplexers.Clear(); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheDefaultStrategyFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheDefaultStrategyFixture.cs new file mode 100644 index 00000000..d5595941 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheDefaultStrategyFixture.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading; +using NHibernate.Cache; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + [TestFixture] + public partial class RedisCacheDefaultStrategyFixture : RedisCacheFixture + { + [Test] + public void TestNoExpiration() + { + var props = GetDefaultProperties(); + props["expiration"] = "0"; + Assert.Throws(() => DefaultProvider.BuildCache(DefaultRegion, props), + "default region strategy should not allow to have no expiration"); + } + + [Test] + public void TestMaxAllowedVersion() + { + var cache = (RedisCache) GetDefaultCache(); + var strategy = (DefaultRegionStrategy)cache.RegionStrategy; + var version = strategy.CurrentVersion; + + var props = GetDefaultProperties(); + props.Add("cache.region_strategy.default.max_allowed_version", version.ToString()); + cache = (RedisCache) DefaultProvider.BuildCache(DefaultRegion, props); + strategy = (DefaultRegionStrategy) cache.RegionStrategy; + + cache.Clear(); + + Assert.That(strategy.CurrentVersion, Is.EqualTo(1L), "the version was not reset to 1"); + } + + [Test] + public void TestClearWithMultipleClientsAndPubSub() + { + const string key = "keyTestClear"; + const string value = "valueClear"; + + var cache = (RedisCache)GetDefaultCache(); + var strategy = (DefaultRegionStrategy)cache.RegionStrategy; + var cache2 = (RedisCache) GetDefaultCache(); + var strategy2 = (DefaultRegionStrategy) cache2.RegionStrategy; + + // add the item + cache.Put(key, value); + + // make sure it's there + var item = cache.Get(key); + Assert.That(item, Is.Not.Null, "couldn't find item in cache"); + + item = cache2.Get(key); + Assert.That(item, Is.Not.Null, "couldn't find item in second cache"); + + var version = strategy.CurrentVersion; + + // clear the cache + cache.Clear(); + + Assert.That(strategy.CurrentVersion, Is.EqualTo(version + 1), "the version has not been updated"); + Thread.Sleep(TimeSpan.FromSeconds(2)); + Assert.That(strategy2.CurrentVersion, Is.EqualTo(version + 1), "the version should be updated with the pub/sub api"); + + // make sure we don't get an item + item = cache.Get(key); + Assert.That(item, Is.Null, "item still exists in cache after clear"); + + item = cache2.Get(key); + Assert.That(item, Is.Null, "item still exists in the second cache after clear"); + } + + [Test] + public void TestClearWithMultipleClientsAndNoPubSub() + { + const string key = "keyTestClear"; + const string value = "valueClear"; + + var props = GetDefaultProperties(); + props.Add("cache.region_strategy.default.use_pubsub", "false"); + + var cache = (RedisCache) DefaultProvider.BuildCache(DefaultRegion, props); + var strategy = (DefaultRegionStrategy) cache.RegionStrategy; + var cache2 = (RedisCache) DefaultProvider.BuildCache(DefaultRegion, props); + var strategy2 = (DefaultRegionStrategy) cache2.RegionStrategy; + + // add the item + cache.Put(key, value); + + // make sure it's there + var item = cache.Get(key); + Assert.That(item, Is.Not.Null, "couldn't find item in cache"); + + item = cache2.Get(key); + Assert.That(item, Is.Not.Null, "couldn't find item in second cache"); + + var version = strategy.CurrentVersion; + + // clear the cache + cache.Clear(); + + Assert.That(strategy.CurrentVersion, Is.EqualTo(version + 1), "the version has not been updated"); + Thread.Sleep(TimeSpan.FromSeconds(2)); + Assert.That(strategy2.CurrentVersion, Is.EqualTo(version), "the version should not be updated"); + + // make sure we don't get an item + item = cache.Get(key); + Assert.That(item, Is.Null, "item still exists in cache after clear"); + + item = cache2.Get(key); + Assert.That(item, Is.Null, "item still exists in the second cache after clear"); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheFastStrategyFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheFastStrategyFixture.cs new file mode 100644 index 00000000..3d3869e4 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheFastStrategyFixture.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + [TestFixture] + public class RedisCacheFastStrategyFixture : RedisCacheFixture + { + protected override bool SupportsClear => false; + + protected override void Configure(Dictionary defaultProperties) + { + base.Configure(defaultProperties); + defaultProperties.Add(RedisEnvironment.RegionStrategy, typeof(FastRegionStrategy).AssemblyQualifiedName); + } + + [Test] + public void TestRegionStrategyType() + { + var cache = (RedisCache)GetDefaultCache(); + Assert.That(cache, Is.Not.Null, "cache is not a redis cache."); + + Assert.That(cache.RegionStrategy, Is.TypeOf(), "cache strategy is not type of FastRegionStrategy"); + } + + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheFixture.cs new file mode 100644 index 00000000..791dfe0d --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheFixture.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Xml; +using System.Xml.Linq; +using NHibernate.Cache; +using NHibernate.Cache.Entry; +using NHibernate.Caches.Common.Tests; +using NHibernate.Collection; +using NHibernate.Engine; +using NHibernate.Persister.Entity; +using NHibernate.Type; +using NSubstitute; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + [TestFixture] + public abstract partial class RedisCacheFixture : CacheFixture + { + protected override bool SupportsSlidingExpiration => true; + protected override bool SupportsLocking => true; + protected override bool SupportsDistinguishingKeysWithSameStringRepresentationAndHashcode => false; + + protected override Func ProviderBuilder => + () => new RedisCacheProvider(); + + [Serializable] + protected class CustomCacheKey + { + private readonly int _hashCode; + + public CustomCacheKey(object id, string entityName, bool useIdHashCode) + { + Id = id; + EntityName = entityName; + _hashCode = useIdHashCode ? id.GetHashCode() : base.GetHashCode(); + } + + public object Id { get; } + + public string EntityName { get; } + + public override string ToString() + { + return EntityName + "#" + Id; + } + + public override bool Equals(object obj) + { + if (!(obj is CustomCacheKey other)) + { + return false; + } + return Equals(other.Id, Id) && Equals(other.EntityName, EntityName); + } + + public override int GetHashCode() + { + return _hashCode.GetHashCode(); + } + } + + [Test] + public void TestEqualObjectsWithDifferentHashCode() + { + var value = "value"; + var obj1 = new CustomCacheKey(1, "test", false); + var obj2 = new CustomCacheKey(1, "test", false); + + var cache = GetDefaultCache(); + + cache.Put(obj1, value); + Assert.That(cache.Get(obj1), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(cache.Get(obj2), Is.EqualTo(value), "Unable to retrieved cached object for key obj2"); + cache.Remove(obj1); + } + + [Test] + public void TestEqualObjectsWithDifferentHashCodeAndUseHashCodeGlobalConfiguration() + { + var value = "value"; + var obj1 = new CustomCacheKey(1, "test", false); + var obj2 = new CustomCacheKey(1, "test", false); + + var props = GetDefaultProperties(); + var cacheProvider = ProviderBuilder(); + props[RedisEnvironment.AppendHashcode] = "true"; + cacheProvider.Start(props); + var cache = cacheProvider.BuildCache(DefaultRegion, props); + + cache.Put(obj1, value); + Assert.That(cache.Get(obj1), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(cache.Get(obj2), Is.Null, "The hash code should be used in the cache key"); + cache.Remove(obj1); + } + + [Test] + public void TestEqualObjectsWithDifferentHashCodeAndUseHashCodeRegionConfiguration() + { + var value = "value"; + var obj1 = new CustomCacheKey(1, "test", false); + var obj2 = new CustomCacheKey(1, "test", false); + + var props = GetDefaultProperties(); + var cacheProvider = ProviderBuilder(); + cacheProvider.Start(props); + props["append-hashcode"] = "true"; + var cache = cacheProvider.BuildCache(DefaultRegion, props); + + cache.Put(obj1, value); + Assert.That(cache.Get(obj1), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(cache.Get(obj2), Is.Null, "The hash code should be used in the cache key"); + cache.Remove(obj1); + } + + [Serializable] + protected class ObjectEqualToString + { + public ObjectEqualToString(int id) + { + Id = id; + } + + public int Id { get; } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public override string ToString() + { + return nameof(ObjectEqualToString); + } + + public override bool Equals(object obj) + { + if (!(obj is ObjectEqualToString other)) + { + return false; + } + + return other.Id == Id; + } + } + + [Test] + public void TestNonEqualObjectsWithEqualToString() + { + var value = "value"; + var obj1 = new CustomCacheKey(new ObjectEqualToString(1), "test", true); + var obj2 = new CustomCacheKey(new ObjectEqualToString(2), "test", true); + + var cache = GetDefaultCache(); + + cache.Put(obj1, value); + Assert.That(cache.Get(obj1), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(cache.Get(obj2), Is.EqualTo(value), "Unable to retrieved cached object for key obj2"); + cache.Remove(obj1); + } + + [Test] + public void TestNonEqualObjectsWithEqualToStringUseHashCode() + { + var value = "value"; + var obj1 = new CustomCacheKey(new ObjectEqualToString(1), "test", true); + var obj2 = new CustomCacheKey(new ObjectEqualToString(2), "test", true); + + var props = GetDefaultProperties(); + var cacheProvider = ProviderBuilder(); + props[RedisEnvironment.AppendHashcode] = "true"; + cacheProvider.Start(props); + var cache = cacheProvider.BuildCache(DefaultRegion, props); + + cache.Put(obj1, value); + Assert.That(cache.Get(obj1), Is.EqualTo(value), "Unable to retrieved cached object for key obj1"); + Assert.That(cache.Get(obj2), Is.Null, "Unexpectedly found a cache entry for key obj2 after obj1 put"); + cache.Remove(obj1); + } + + [Test] + public void TestEnvironmentName() + { + var props = GetDefaultProperties(); + + var developProvider = ProviderBuilder(); + props[RedisEnvironment.EnvironmentName] = "develop"; + developProvider.Start(props); + var developCache = developProvider.BuildCache(DefaultRegion, props); + + var releaseProvider = ProviderBuilder(); + props[RedisEnvironment.EnvironmentName] = "release"; + releaseProvider.Start(props); + var releaseCache = releaseProvider.BuildCache(DefaultRegion, props); + + const string key = "testKey"; + const string value = "testValue"; + + developCache.Put(key, value); + + Assert.That(releaseCache.Get(key), Is.Null, "release environment should be separate from develop"); + + developCache.Remove(key); + releaseCache.Put(key, value); + + Assert.That(developCache.Get(key), Is.Null, "develop environment should be separate from release"); + + releaseCache.Remove(key); + + developProvider.Stop(); + releaseProvider.Stop(); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCachePerformanceFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCachePerformanceFixture.cs new file mode 100644 index 00000000..ead798d2 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCachePerformanceFixture.cs @@ -0,0 +1,419 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using NHibernate.Cache; +using NHibernate.Caches.Common.Tests; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + [TestFixture, Explicit] + public partial class RedisCachePerformanceFixture : Fixture + { + private const int RepeatTimes = 5; + private const int BatchSize = 20; + private const int CacheItems = 1000; + + protected override Func ProviderBuilder => + () => new RedisCacheProvider(); + + [Test] + public void TestGetOperation() + { + TestOperation("Get", true, (cache, key, _) => cache.Get(key)); + } + + [Test] + public void TestGetManyOperation() + { + TestBatchOperation("GetMany", true, (cache, keys, _) => cache.GetMany(keys), BatchSize); + } + + [Test] + public void TestGetOperationWithSlidingExpiration() + { + var props = new Dictionary {{"sliding", "true"}}; + TestOperation("Get", true, (cache, key, _) => cache.Get(key), + caches: new List {GetDefaultRedisCache(props), GetFastRedisCache(props)}); + } + + [Test] + public void TestGetManyOperationWithSlidingExpiration() + { + var props = new Dictionary {{"sliding", "true"}}; + TestBatchOperation("GetMany", true, (cache, keys, _) => cache.GetMany(keys), + batchSize: BatchSize, + caches: new List {GetDefaultRedisCache(props), GetFastRedisCache(props)}); + } + + [Test] + public void TestPutOperation() + { + var props = new Dictionary {{"expiration", "0"}}; + TestOperation("Put", false, (cache, key, value) => cache.Put(key, value), + caches: new List {GetFastRedisCache(props)}); + } + + [Test] + public void TestPutManyOperation() + { + var props = new Dictionary {{"expiration", "0"}}; + TestBatchOperation("PutMany", false, (cache, keys, values) => cache.PutMany(keys, values), + batchSize: null, + caches: new List {GetFastRedisCache(props)}); + } + + [Test] + public void TestPutOperationWithExpiration() + { + TestOperation("Put", false, (cache, key, value) => cache.Put(key, value)); + } + + [Test] + public void TestPutManyOperationWithExpiration() + { + TestBatchOperation("PutMany", false, (cache, keys, values) => cache.PutMany(keys, values), null); + } + + [Test] + public void TestLockUnlockOperation() + { + TestOperation("Lock/Unlock", true, (cache, key, _) => + { + var value = cache.Lock(key); + cache.Unlock(key, value); + }); + } + + [Test] + public void TestLockUnlockManyOperation() + { + TestBatchOperation("LockMany/UnlockMany", true, (cache, keys, _) => + { + var value = cache.LockMany(keys); + cache.UnlockMany(keys, value); + }, null); + } + + private void TestBatchOperation(string operation, bool fillData, + Action keyValueAction, int? batchSize, + int? cacheItems = null, int? repeat = null, List caches = null) + { + TestOperation(operation, fillData, null, keyValueAction, batchSize, cacheItems, repeat, caches); + } + + private Task TestBatchOperationAsync(string operation, bool fillData, + Func keyValueAction, int? batchSize, + int? cacheItems = null, int? repeat = null, List caches = null) + { + return TestOperationAsync(operation, fillData, null, keyValueAction, batchSize, cacheItems, repeat, caches); + } + + private void TestOperation(string operation, bool fillData, + Action> keyValueAction, + int? cacheItems = null, int? repeat = null, List caches = null) + { + TestOperation(operation, fillData, keyValueAction, null, null, cacheItems, repeat, caches); + } + + private Task TestOperationAsync(string operation, bool fillData, + Func, Task> keyValueAction, + int? cacheItems = null, int? repeat = null, List caches = null) + { + return TestOperationAsync(operation, fillData, keyValueAction, null, null, cacheItems, repeat, caches); + } + + private void TestOperation(string operation, bool fillData, + Action> keyValueAction, + Action batchKeyValueAction, + int? batchSize, int? cacheItems = null, int? repeat = null, List caches = null + ) + { + caches = caches ?? new List {GetDefaultRedisCache(), GetFastRedisCache()}; + var cacheData = GetCacheData(cacheItems ?? CacheItems); + + if (fillData) + { + foreach (var cache in caches) + { + PutCacheData(cache, cacheData); + } + } + + foreach (var cache in caches) + { + var repeatPolicy = new CacheOperationRepeatPolicy(operation, cache, repeat ?? RepeatTimes, cacheData); + if (keyValueAction != null) + { + repeatPolicy.Execute(keyValueAction); + } + else + { + repeatPolicy.BatchExecute(batchKeyValueAction, batchSize); + } + } + + foreach (var cache in caches) + { + RemoveCacheData(cache, cacheData); + } + } + + private async Task TestOperationAsync(string operation, bool fillData, + Func, Task> keyValueAction, + Func batchKeyValueAction, + int? batchSize, int? cacheItems = null, int? repeat = null, List caches = null + ) + { + caches = caches ?? new List { GetDefaultRedisCache(), GetFastRedisCache() }; + var cacheData = GetCacheData(cacheItems ?? CacheItems); + + if (fillData) + { + foreach (var cache in caches) + { + PutCacheData(cache, cacheData); + } + } + + foreach (var cache in caches) + { + var repeatPolicy = new CacheOperationRepeatPolicy(operation, cache, repeat ?? RepeatTimes, cacheData); + if (keyValueAction != null) + { + await repeatPolicy.ExecuteAsync(keyValueAction); + } + else + { + await repeatPolicy.BatchExecuteAsync(batchKeyValueAction, batchSize); + } + } + + foreach (var cache in caches) + { + RemoveCacheData(cache, cacheData); + } + } + + private void PutCacheData(CacheBase cache, Dictionary> cacheData) + { + foreach (var pair in cacheData) + { + cache.Put(pair.Key, pair.Value); + } + } + + private void RemoveCacheData(CacheBase cache, Dictionary> cacheData) + { + foreach (var pair in cacheData) + { + cache.Remove(pair.Key); + } + } + + private Dictionary> GetCacheData(int numberOfKeys) + { + var keyValues = new Dictionary>(); + for (var i = 0; i < numberOfKeys; i++) + { + keyValues.Add( + new CacheKey((long) i, NHibernateUtil.Int64, nameof(GetCacheData), null), + new List + { + i, + string.Join("", Enumerable.Repeat(i, 30)), + Enumerable.Repeat((byte) i, 30).ToArray(), + null, + DateTime.Now, + i / 4.5, + Guid.NewGuid() + } + ); + } + + return keyValues; + } + + private RedisCache GetFastRedisCache(Dictionary properties = null) + { + var props = GetDefaultProperties(); + foreach (var property in properties ?? new Dictionary()) + { + props[property.Key] = property.Value; + } + props["strategy"] = typeof(FastRegionStrategy).AssemblyQualifiedName; + return (RedisCache)DefaultProvider.BuildCache(DefaultRegion, props); + } + + private RedisCache GetDefaultRedisCache(Dictionary properties = null) + { + var props = GetDefaultProperties(); + foreach (var property in properties ?? new Dictionary()) + { + props[property.Key] = property.Value; + } + return (RedisCache) DefaultProvider.BuildCache(DefaultRegion, props); + } + } + + public class CacheOperationRepeatPolicy + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(CacheOperationRepeatPolicy)); + private readonly string _operation; + private readonly RedisCache _cache; + private readonly int _repeatTimes; + private readonly Dictionary> _cacheData; + + public CacheOperationRepeatPolicy(string operation, RedisCache cache, int repeatTimes, Dictionary> cacheData) + { + _operation = operation; + _cache = cache; + _repeatTimes = repeatTimes; + _cacheData = cacheData; + } + + public void BatchExecute(Action keyValueAction, int? batchSize) + { + var batchKeys = new List(); + var batchValues = new List(); + // Cold start + Iterate(); + + var timer = new Stopwatch(); + var result = new long[_repeatTimes]; + for (var i = 0; i < _repeatTimes; i++) + { + timer.Restart(); + Iterate(); + timer.Stop(); + result[i] = timer.ElapsedMilliseconds; + } + LogResult(result, batchSize); + + void Iterate() + { + foreach (var pair in _cacheData) + { + if (batchSize.HasValue && batchKeys.Count > 0 && batchKeys.Count % batchSize == 0) + { + keyValueAction(_cache, batchKeys.ToArray(), batchValues.ToArray()); + batchKeys.Clear(); + batchValues.Clear(); + } + batchKeys.Add(pair.Key); + batchValues.Add(pair.Value); + } + + if (batchKeys.Count == 0) + { + return; + } + keyValueAction(_cache, batchKeys.ToArray(), batchValues.ToArray()); + batchKeys.Clear(); + batchValues.Clear(); + } + } + + public async Task BatchExecuteAsync(Func keyValueFunc, int? batchSize) + { + var batchKeys = new List(); + var batchValues = new List(); + // Cold start + await Iterate(); + + var timer = new Stopwatch(); + var result = new long[_repeatTimes]; + for (var i = 0; i < _repeatTimes; i++) + { + timer.Restart(); + await Iterate(); + timer.Stop(); + result[i] = timer.ElapsedMilliseconds; + } + LogResult(result, batchSize); + + async Task Iterate() + { + foreach (var pair in _cacheData) + { + if (batchSize.HasValue && batchKeys.Count > 0 && batchKeys.Count % batchSize == 0) + { + await keyValueFunc(_cache, batchKeys.ToArray(), batchValues.ToArray()); + batchKeys.Clear(); + batchValues.Clear(); + } + batchKeys.Add(pair.Key); + batchValues.Add(pair.Value); + } + + if (batchKeys.Count == 0) + { + return; + } + await keyValueFunc(_cache, batchKeys.ToArray(), batchValues.ToArray()); + batchKeys.Clear(); + batchValues.Clear(); + } + } + + public void Execute(Action> keyValueAction) + { + // Cold start + foreach (var pair in _cacheData) + { + keyValueAction(_cache, pair.Key, pair.Value); + } + + var timer = new Stopwatch(); + var result = new long[_repeatTimes]; + for (var i = 0; i < _repeatTimes; i++) + { + timer.Restart(); + foreach (var pair in _cacheData) + { + keyValueAction(_cache, pair.Key, pair.Value); + } + timer.Stop(); + result[i] = timer.ElapsedMilliseconds; + } + LogResult(result, 1); + } + + public async Task ExecuteAsync(Func, Task> keyValueFunc) + { + // Cold start + foreach (var pair in _cacheData) + { + await keyValueFunc(_cache, pair.Key, pair.Value); + } + + var timer = new Stopwatch(); + var result = new long[_repeatTimes]; + for (var i = 0; i < _repeatTimes; i++) + { + timer.Restart(); + foreach (var pair in _cacheData) + { + await keyValueFunc(_cache, pair.Key, pair.Value); + } + timer.Stop(); + result[i] = timer.ElapsedMilliseconds; + } + LogResult(result, 1); + } + + private void LogResult(long[] result, int? batchSize) + { + Log.Info( + $"{_operation} operation for {_cacheData.Count} keys with region strategy {_cache.RegionStrategy.GetType().Name}:{Environment.NewLine}" + + $"Total iterations: {_repeatTimes}{Environment.NewLine}" + + $"Batch size: {batchSize}{Environment.NewLine}" + + $"Times per iteration {string.Join(",", result.Select(o => $"{o}ms"))}{Environment.NewLine}" + + $"Average {result.Average()}ms" + + ); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheProviderFixture.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheProviderFixture.cs new file mode 100644 index 00000000..440d57a5 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/RedisCacheProviderFixture.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using NHibernate.Cache; +using NHibernate.Caches.Common; +using NHibernate.Caches.Common.Tests; +using NSubstitute; +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + [TestFixture] + public class RedisCacheProviderFixture : CacheProviderFixture + { + protected override Func ProviderBuilder => + () => new RedisCacheProvider(); + + [Test] + public void TestBuildCacheFromConfig() + { + var cache = DefaultProvider.BuildCache("foo", null); + Assert.That(cache, Is.Not.Null, "pre-configured cache not found"); + } + + [Test] + public void TestExpiration() + { + var cache = DefaultProvider.BuildCache("foo", null) as RedisCache; + Assert.That(cache, Is.Not.Null, "pre-configured foo cache not found"); + + var strategy = cache.RegionStrategy; + Assert.That(strategy, Is.Not.Null, "strategy was not set for the pre-configured foo cache"); + Assert.That(strategy, Is.TypeOf(), "unexpected strategy type for foo region"); + Assert.That(strategy.Expiration, Is.EqualTo(TimeSpan.FromSeconds(500)), "unexpected expiration value for foo region"); + + cache = (RedisCache) DefaultProvider.BuildCache("noExplicitExpiration", null); + Assert.That(cache.RegionStrategy.Expiration, Is.EqualTo(TimeSpan.FromSeconds(300)), + "unexpected expiration value for noExplicitExpiration region"); + Assert.That(cache.RegionStrategy.UseSlidingExpiration, Is.True, "unexpected sliding value for noExplicitExpiration region"); + + cache = (RedisCache) DefaultProvider + .BuildCache("noExplicitExpiration", new Dictionary { { "expiration", "100" } }); + Assert.That(cache.RegionStrategy.Expiration, Is.EqualTo(TimeSpan.FromSeconds(100)), + "unexpected expiration value for noExplicitExpiration region with default expiration"); + + cache = (RedisCache) DefaultProvider + .BuildCache("noExplicitExpiration", new Dictionary { { Cfg.Environment.CacheDefaultExpiration, "50" } }); + Assert.That(cache.RegionStrategy.Expiration, Is.EqualTo(TimeSpan.FromSeconds(50)), + "unexpected expiration value for noExplicitExpiration region with cache.default_expiration"); + } + + [Test] + public void TestDefaultCacheConfiguration() + { + var connectionProvider = Substitute.For(); + var databaseProvider = Substitute.For(); + var retryDelayProvider = Substitute.For(); + var lockValueProvider = Substitute.For(); + var regionStrategyFactory = Substitute.For(); + var serializer = Substitute.For(); + + var defaultConfig = RedisCacheProvider.DefaultCacheConfiguration; + defaultConfig.ConnectionMultiplexerProvider = connectionProvider; + defaultConfig.DatabaseProvider = databaseProvider; + defaultConfig.LockConfiguration.ValueProvider = lockValueProvider; + defaultConfig.LockConfiguration.RetryDelayProvider = retryDelayProvider; + defaultConfig.RegionStrategyFactory = regionStrategyFactory; + defaultConfig.Serializer = serializer; + + var provider = (RedisCacheProvider) GetNewProvider(); + var config = provider.CacheConfiguration; + + Assert.That(config.ConnectionMultiplexerProvider, Is.EqualTo(connectionProvider)); + Assert.That(config.DatabaseProvider, Is.EqualTo(databaseProvider)); + Assert.That(config.LockConfiguration.RetryDelayProvider, Is.EqualTo(retryDelayProvider)); + Assert.That(config.LockConfiguration.ValueProvider, Is.EqualTo(lockValueProvider)); + Assert.That(config.RegionStrategyFactory, Is.EqualTo(regionStrategyFactory)); + Assert.That(config.Serializer, Is.EqualTo(serializer)); + + RedisCacheProvider.DefaultCacheConfiguration = new RedisCacheConfiguration(); + } + + [Test] + public void TestUserProvidedObjectsFactory() + { + var originalObjectsFactory = Cfg.Environment.ObjectsFactory; + try + { + var customObjectsFactory = new CustomObjectsFactory(); + Cfg.Environment.ObjectsFactory = customObjectsFactory; + + var connectionProvider = Substitute.For(); + var databaseProvider = Substitute.For(); + var retryDelayProvider = Substitute.For(); + var lockValueProvider = Substitute.For(); + var regionStrategyFactory = Substitute.For(); + var serializer = Substitute.For(); + + customObjectsFactory.RegisterSingleton(connectionProvider); + customObjectsFactory.RegisterSingleton(databaseProvider); + customObjectsFactory.RegisterSingleton(retryDelayProvider); + customObjectsFactory.RegisterSingleton(lockValueProvider); + customObjectsFactory.RegisterSingleton(regionStrategyFactory); + customObjectsFactory.RegisterSingleton(serializer); + + var provider = (RedisCacheProvider) GetNewProvider(); + var config = provider.CacheConfiguration; + + Assert.That(config.ConnectionMultiplexerProvider, Is.EqualTo(connectionProvider)); + Assert.That(config.DatabaseProvider, Is.EqualTo(databaseProvider)); + Assert.That(config.LockConfiguration.RetryDelayProvider, Is.EqualTo(retryDelayProvider)); + Assert.That(config.LockConfiguration.ValueProvider, Is.EqualTo(lockValueProvider)); + Assert.That(config.RegionStrategyFactory, Is.EqualTo(regionStrategyFactory)); + Assert.That(config.Serializer, Is.EqualTo(serializer)); + } + finally + { + Cfg.Environment.ObjectsFactory = originalObjectsFactory; + } + } + + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/TestsContext.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/TestsContext.cs new file mode 100644 index 00000000..3d00f4ab --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.Tests/TestsContext.cs @@ -0,0 +1,42 @@ +using log4net.Repository.Hierarchy; +#if !NETFX +using NHibernate.Caches.Common.Tests; +#endif +using NUnit.Framework; + +namespace NHibernate.Caches.StackExchangeRedis.Tests +{ + [SetUpFixture] + public class TestsContext + { + [OneTimeSetUp] + public void RunBeforeAnyTests() + { +#if !NETFX + TestsContextHelper.RunBeforeAnyTests(typeof(TestsContext).Assembly, "redis"); +#endif + ConfigureLog4Net(); + } + +#if !NETFX + [OneTimeTearDown] + public void RunAfterAnyTests() + { + TestsContextHelper.RunAfterAnyTests(); + } +#endif + + private static void ConfigureLog4Net() + { + var hierarchy = (Hierarchy)log4net.LogManager.GetRepository(typeof(TestsContext).Assembly); + + var consoleAppender = new log4net.Appender.ConsoleAppender + { + Layout = new log4net.Layout.PatternLayout("%d{ABSOLUTE} %-5p %c{1}:%L - %m%n"), + }; + hierarchy.Root.Level = log4net.Core.Level.Info; + hierarchy.Root.AddAppender(consoleAppender); + hierarchy.Configured = true; + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/AbstractRegionStrategy.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/AbstractRegionStrategy.cs new file mode 100644 index 00000000..a7f8dd2d --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/AbstractRegionStrategy.cs @@ -0,0 +1,537 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Caches.Common; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// An abstract region strategy that provides common functionalities to create a region strategy. + /// + public abstract partial class AbstractRegionStrategy + { + /// + /// The NHibernate logger. + /// + protected readonly INHibernateLogger Log; + + /// + /// The Redis connection. + /// + protected readonly IConnectionMultiplexer ConnectionMultiplexer; + + /// + /// The Redis database where the keys are stored. + /// + protected readonly IDatabase Database; + + /// + /// The instance. + /// + protected readonly CacheSerializerBase Serializer; + + private readonly RedisKeyLocker _keyLocker; + + /// + /// The constructor for creating the region strategy. + /// + /// The Redis connection. + /// The region configuration. + /// The NHibernate configuration properties. + protected AbstractRegionStrategy(IConnectionMultiplexer connectionMultiplexer, + RedisCacheRegionConfiguration configuration, IDictionary properties) + { + Log = NHibernateLogger.For(GetType()); + RegionName = configuration.RegionName; + Expiration = configuration.Expiration; + UseSlidingExpiration = configuration.UseSlidingExpiration; + AppendHashcode = configuration.AppendHashcode; + RegionKey = configuration.RegionKey; + ConnectionMultiplexer = connectionMultiplexer; + Database = configuration.DatabaseProvider.Get(connectionMultiplexer, configuration.Database); + Serializer = configuration.Serializer; + LockTimeout = configuration.LockConfiguration.KeyTimeout; + _keyLocker = new RedisKeyLocker(RegionName, Database, configuration.LockConfiguration); + } + + /// + /// The lua script for getting a key from the cache. + /// + protected virtual string GetScript => null; + + /// + /// The lua script for getting many keys from the cache at once. + /// + protected virtual string GetManyScript => null; + + /// + /// The lua script for putting a key into the cache. + /// + protected virtual string PutScript => null; + + /// + /// The lua script for putting many keys into the cache at once. + /// + protected abstract string PutManyScript { get; } + + /// + /// The lua script for removing a key from the cache. + /// + protected virtual string RemoveScript => null; + + /// + /// The lua script for locking a key. + /// + protected virtual string LockScript => null; + + /// + /// The lua script for locking many keys at once. + /// + protected abstract string LockManyScript { get; } + + /// + /// The lua script for unlocking a key. + /// + protected virtual string UnlockScript => null; + + /// + /// The lua script for unlocking many keys at once. + /// + protected abstract string UnlockManyScript { get; } + + /// + /// The expiration delay applied to cached items. + /// + public TimeSpan Expiration { get; } + + /// + /// The name of the region. + /// + public string RegionName { get; } + + /// + /// The key representing the region that is composed of , + /// , + /// and . + /// + public string RegionKey { get; } + + /// + /// Should the expiration delay be sliding? + /// + /// for resetting a cached item expiration each time it is accessed. + public bool UseSlidingExpiration { get; } + + /// + /// Whether the hash code of the key should be added to the cache key. + /// + public bool AppendHashcode { get; } + + /// + /// Is the expiration enabled? + /// + public bool ExpirationEnabled => Expiration != TimeSpan.Zero; + + /// + /// The timeout of an acquired lock. + /// + public TimeSpan LockTimeout { get; } + + /// + /// Gets the object that is stored in Redis by its key. + /// + /// The key of the object to retrieve. + /// The object behind the key or if the key was not found. + public virtual object Get(object key) + { + if (key == null) + { + return null; + } + + var cacheKey = GetCacheKey(key); + Log.Debug("Fetching object with key: '{0}'.", cacheKey); + RedisValue result; + if (string.IsNullOrEmpty(GetScript)) + { + result = Database.StringGet(cacheKey); + } + else + { + var keys = AppendAdditionalKeys(new RedisKey[] {cacheKey}); + var values = AppendAdditionalValues(new RedisValue[] + { + UseSlidingExpiration && ExpirationEnabled, + (long) Expiration.TotalMilliseconds + }); + var results = (RedisValue[]) Database.ScriptEvaluate(GetScript, keys, values); + result = results[0]; + } + + return result.IsNullOrEmpty ? null : Serializer.Deserialize(result); + } + + /// + /// Gets the objects that are stored in Redis by their key. + /// + /// The keys of the objects to retrieve. + /// An array of objects behind the keys or if the key was not found. + public virtual object[] GetMany(object[] keys) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + + var cacheKeys = new RedisKey[keys.Length]; + Log.Debug("Fetching {0} objects...", keys.Length); + for (var i = 0; i < keys.Length; i++) + { + cacheKeys[i] = GetCacheKey(keys[i]); + Log.Debug("Fetching object with key: '{0}'.", cacheKeys[i]); + } + + RedisValue[] results; + if (string.IsNullOrEmpty(GetManyScript)) + { + results = Database.StringGet(cacheKeys); + } + else + { + cacheKeys = AppendAdditionalKeys(cacheKeys); + var values = AppendAdditionalValues(new RedisValue[] + { + UseSlidingExpiration && ExpirationEnabled, + (long) Expiration.TotalMilliseconds + }); + results = (RedisValue[]) Database.ScriptEvaluate(GetManyScript, cacheKeys, values); + } + + var objects = new object[keys.Length]; + for (var i = 0; i < results.Length; i++) + { + var result = results[i]; + if (!result.IsNullOrEmpty) + { + objects[i] = Serializer.Deserialize(result); + } + } + + return objects; + } + + /// + /// Stores the object into Redis by the given key. + /// + /// The key to store the object. + /// The object to store. + public virtual void Put(object key, object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var cacheKey = GetCacheKey(key); + Log.Debug("Putting object with key: '{0}'.", cacheKey); + RedisValue serializedValue = Serializer.Serialize(value); + + if (string.IsNullOrEmpty(PutScript)) + { + Database.StringSet(cacheKey, serializedValue, ExpirationEnabled ? Expiration : (TimeSpan?) null); + return; + } + + var keys = AppendAdditionalKeys(new RedisKey[] {cacheKey}); + var values = AppendAdditionalValues(new[] + { + serializedValue, + ExpirationEnabled, + (long) Expiration.TotalMilliseconds + }); + Database.ScriptEvaluate(PutScript, keys, values); + } + + /// + /// Stores the objects into Redis by the given keys. + /// + /// The keys to store the objects. + /// The objects to store. + public virtual void PutMany(object[] keys, object[] values) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (keys.Length != values.Length) + { + throw new ArgumentException($"Length of {nameof(keys)} array does not match with {nameof(values)} array."); + } + + Log.Debug("Putting {0} objects...", keys.Length); + if (string.IsNullOrEmpty(PutManyScript)) + { + if (ExpirationEnabled) + { + throw new NotSupportedException($"{nameof(PutMany)} operation with expiration is not supported."); + } + + var pairs = new KeyValuePair[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + pairs[i] = new KeyValuePair(GetCacheKey(keys[i]), Serializer.Serialize(values[i])); + Log.Debug("Putting object with key: '{0}'.", pairs[i].Key); + } + + Database.StringSet(pairs); + return; + } + + + var cacheKeys = new RedisKey[keys.Length]; + var cacheValues = new RedisValue[keys.Length + 2]; + for (var i = 0; i < keys.Length; i++) + { + cacheKeys[i] = GetCacheKey(keys[i]); + cacheValues[i] = Serializer.Serialize(values[i]); + Log.Debug("Putting object with key: '{0}'.", cacheKeys[i]); + } + + cacheKeys = AppendAdditionalKeys(cacheKeys); + cacheValues[keys.Length] = ExpirationEnabled; + cacheValues[keys.Length + 1] = (long) Expiration.TotalMilliseconds; + cacheValues = AppendAdditionalValues(cacheValues); + Database.ScriptEvaluate(PutManyScript, cacheKeys, cacheValues); + } + + /// + /// Removes the key from Redis. + /// + /// The key to remove. + public virtual bool Remove(object key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var cacheKey = GetCacheKey(key); + Log.Debug("Removing object with key: '{0}'.", cacheKey); + if (string.IsNullOrEmpty(RemoveScript)) + { + return Database.KeyDelete(cacheKey); + } + + var keys = AppendAdditionalKeys(new RedisKey[] {cacheKey}); + var values = GetAdditionalValues(); + var results = (RedisValue[]) Database.ScriptEvaluate(RemoveScript, keys, values); + return (bool) results[0]; + } + + /// + /// Locks the key. + /// + /// The key to lock. + /// The value used to lock the key. + public virtual string Lock(object key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var cacheKey = GetCacheKey(key); + Log.Debug("Locking object with key: '{0}'.", cacheKey); + return _keyLocker.Lock(cacheKey, LockScript, GetAdditionalKeys(), GetAdditionalValues()); + } + + /// + /// Locks many keys at once. + /// + /// The keys to lock. + /// The value used to lock the keys. + public virtual string LockMany(object[] keys) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + + if (string.IsNullOrEmpty(LockManyScript)) + { + throw new NotSupportedException($"{nameof(LockMany)} operation is not supported."); + } + + Log.Debug("Locking {0} objects...", keys.Length); + var cacheKeys = new string[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + cacheKeys[i] = GetCacheKey(keys[i]); + Log.Debug("Locking object with key: '{0}'.", cacheKeys[i]); + } + + return _keyLocker.LockMany(cacheKeys, LockManyScript, GetAdditionalKeys(), GetAdditionalValues()); + } + + /// + /// Unlocks the key. + /// + /// The key to unlock. + /// The value used to lock the key. + /// Whether the key was unlocked + public virtual bool Unlock(object key, string lockValue) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var cacheKey = GetCacheKey(key); + Log.Debug("Unlocking object with key: '{0}'.", cacheKey); + var unlocked = _keyLocker.Unlock(cacheKey, lockValue, UnlockScript, GetAdditionalKeys(), GetAdditionalValues()); + Log.Debug("Unlock key '{0}' result: {1}", cacheKey, unlocked); + return unlocked; + } + + /// + /// Unlocks many keys at once. + /// + /// The keys to unlock. + /// The value used to lock the keys. + /// The number of unlocked keys. + public virtual int UnlockMany(object[] keys, string lockValue) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + + if (string.IsNullOrEmpty(UnlockManyScript)) + { + throw new NotSupportedException($"{nameof(UnlockMany)} operation is not supported."); + } + + Log.Debug("Unlocking {0} objects...", keys.Length); + var cacheKeys = new string[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + cacheKeys[i] = GetCacheKey(keys[i]); + Log.Debug("Unlocking object with key: '{0}'.", cacheKeys[i]); + } + + var unlockedKeys = + _keyLocker.UnlockMany(cacheKeys, lockValue, UnlockManyScript, GetAdditionalKeys(), GetAdditionalValues()); + if (Log.IsDebugEnabled()) + { + Log.Debug("Number of unlocked objects with keys ({0}): {1}", string.Join(",", cacheKeys.Select(o => $"'{o}'")), + unlockedKeys); + } + + return unlockedKeys; + } + + /// + /// Clears all the keys from the region. + /// + public abstract void Clear(); + + /// + /// Validates if the region strategy was correctly configured. + /// + /// Thrown when the region strategy is not configured correctly. + public abstract void Validate(); + + /// + /// Gets additional values required by the concrete strategy that can be used in the lua scripts. + /// + /// The values to be used in the lua scripts. + protected virtual RedisValue[] GetAdditionalValues() + { + return null; + } + + /// + /// Gets additional keys required by the concrete strategy that can be used in the lua scripts. + /// + /// The keys to be used in the lua scripts. + protected virtual RedisKey[] GetAdditionalKeys() + { + return null; + } + + /// + /// Calculates the cache key for the given object. + /// + /// The object for which the key will be calculated. + /// The key for the given object. + protected virtual string GetCacheKey(object value) + { + // Hash tag (wrap with curly brackets) the region key in order to ensure that all region keys + // will be located on the same server, when a Redis cluster is used. + return AppendHashcode + ? string.Concat("{", RegionKey, "}:", value.ToString(), "@", value.GetHashCode()) + : string.Concat("{", RegionKey, "}:", value.ToString()); + } + + /// + /// Combine the given values with the values returned from . + /// + /// The values to combine with the additional values. + /// An array of combined values. + protected RedisValue[] AppendAdditionalValues(RedisValue[] values) + { + if (values == null) + { + return GetAdditionalValues(); + } + + var additionalValues = GetAdditionalValues(); + if (additionalValues == null) + { + return values; + } + + var combinedValues = new RedisValue[values.Length + additionalValues.Length]; + values.CopyTo(combinedValues, 0); + additionalValues.CopyTo(combinedValues, values.Length); + return combinedValues; + } + + /// + /// Combine the given keys with the keys returned from . + /// + /// The keys to combine with the additional keys. + /// An array of combined keys. + protected RedisKey[] AppendAdditionalKeys(RedisKey[] keys) + { + if (keys == null) + { + return GetAdditionalKeys(); + } + + var additionalKeys = GetAdditionalKeys(); + if (additionalKeys == null) + { + return keys; + } + + var combinedKeys = new RedisKey[keys.Length + additionalKeys.Length]; + keys.CopyTo(combinedKeys, 0); + additionalKeys.CopyTo(combinedKeys, keys.Length); + return combinedKeys; + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/AbstractRegionStrategy.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/AbstractRegionStrategy.cs new file mode 100644 index 00000000..e03ddc1f --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/AbstractRegionStrategy.cs @@ -0,0 +1,425 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Caches.Common; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + using System.Threading.Tasks; + using System.Threading; + public abstract partial class AbstractRegionStrategy + { + + /// + /// Gets the object that is stored in Redis by its key. + /// + /// The key of the object to retrieve. + /// A cancellation token that can be used to cancel the work + /// The object behind the key or if the key was not found. + public virtual async Task GetAsync(object key, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (key == null) + { + return null; + } + + var cacheKey = GetCacheKey(key); + Log.Debug("Fetching object with key: '{0}'.", cacheKey); + RedisValue result; + if (string.IsNullOrEmpty(GetScript)) + { + cancellationToken.ThrowIfCancellationRequested(); + result = await (Database.StringGetAsync(cacheKey)).ConfigureAwait(false); + } + else + { + var keys = AppendAdditionalKeys(new RedisKey[] {cacheKey}); + var values = AppendAdditionalValues(new RedisValue[] + { + UseSlidingExpiration && ExpirationEnabled, + (long) Expiration.TotalMilliseconds + }); + cancellationToken.ThrowIfCancellationRequested(); + var results = (RedisValue[]) await (Database.ScriptEvaluateAsync(GetScript, keys, values)).ConfigureAwait(false); + result = results[0]; + } + + return result.IsNullOrEmpty ? null : Serializer.Deserialize(result); + } + + /// + /// Gets the objects that are stored in Redis by their key. + /// + /// The keys of the objects to retrieve. + /// A cancellation token that can be used to cancel the work + /// An array of objects behind the keys or if the key was not found. + public virtual Task GetManyAsync(object[] keys, CancellationToken cancellationToken) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalGetManyAsync(); + async Task InternalGetManyAsync() + { + + var cacheKeys = new RedisKey[keys.Length]; + Log.Debug("Fetching {0} objects...", keys.Length); + for (var i = 0; i < keys.Length; i++) + { + cacheKeys[i] = GetCacheKey(keys[i]); + Log.Debug("Fetching object with key: '{0}'.", cacheKeys[i]); + } + + RedisValue[] results; + if (string.IsNullOrEmpty(GetManyScript)) + { + cancellationToken.ThrowIfCancellationRequested(); + results = await (Database.StringGetAsync(cacheKeys)).ConfigureAwait(false); + } + else + { + cacheKeys = AppendAdditionalKeys(cacheKeys); + var values = AppendAdditionalValues(new RedisValue[] + { + UseSlidingExpiration && ExpirationEnabled, + (long) Expiration.TotalMilliseconds + }); + cancellationToken.ThrowIfCancellationRequested(); + results = (RedisValue[]) await (Database.ScriptEvaluateAsync(GetManyScript, cacheKeys, values)).ConfigureAwait(false); + } + + var objects = new object[keys.Length]; + for (var i = 0; i < results.Length; i++) + { + var result = results[i]; + if (!result.IsNullOrEmpty) + { + objects[i] = Serializer.Deserialize(result); + } + } + + return objects; + } + } + + /// + /// Stores the object into Redis by the given key. + /// + /// The key to store the object. + /// The object to store. + /// A cancellation token that can be used to cancel the work + public virtual Task PutAsync(object key, object value, CancellationToken cancellationToken) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalPutAsync(); + async Task InternalPutAsync() + { + + var cacheKey = GetCacheKey(key); + Log.Debug("Putting object with key: '{0}'.", cacheKey); + RedisValue serializedValue = Serializer.Serialize(value); + + if (string.IsNullOrEmpty(PutScript)) + { + cancellationToken.ThrowIfCancellationRequested(); + await (Database.StringSetAsync(cacheKey, serializedValue, ExpirationEnabled ? Expiration : (TimeSpan?) null)).ConfigureAwait(false); + return; + } + + var keys = AppendAdditionalKeys(new RedisKey[] {cacheKey}); + var values = AppendAdditionalValues(new[] + { + serializedValue, + ExpirationEnabled, + (long) Expiration.TotalMilliseconds + }); + cancellationToken.ThrowIfCancellationRequested(); + await (Database.ScriptEvaluateAsync(PutScript, keys, values)).ConfigureAwait(false); + } + } + + /// + /// Stores the objects into Redis by the given keys. + /// + /// The keys to store the objects. + /// The objects to store. + /// A cancellation token that can be used to cancel the work + public virtual Task PutManyAsync(object[] keys, object[] values, CancellationToken cancellationToken) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (keys.Length != values.Length) + { + throw new ArgumentException($"Length of {nameof(keys)} array does not match with {nameof(values)} array."); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalPutManyAsync(); + async Task InternalPutManyAsync() + { + + Log.Debug("Putting {0} objects...", keys.Length); + if (string.IsNullOrEmpty(PutManyScript)) + { + if (ExpirationEnabled) + { + throw new NotSupportedException($"{nameof(PutManyAsync)} operation with expiration is not supported."); + } + + var pairs = new KeyValuePair[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + pairs[i] = new KeyValuePair(GetCacheKey(keys[i]), Serializer.Serialize(values[i])); + Log.Debug("Putting object with key: '{0}'.", pairs[i].Key); + } + cancellationToken.ThrowIfCancellationRequested(); + + await (Database.StringSetAsync(pairs)).ConfigureAwait(false); + return; + } + + + var cacheKeys = new RedisKey[keys.Length]; + var cacheValues = new RedisValue[keys.Length + 2]; + for (var i = 0; i < keys.Length; i++) + { + cacheKeys[i] = GetCacheKey(keys[i]); + cacheValues[i] = Serializer.Serialize(values[i]); + Log.Debug("Putting object with key: '{0}'.", cacheKeys[i]); + } + + cacheKeys = AppendAdditionalKeys(cacheKeys); + cacheValues[keys.Length] = ExpirationEnabled; + cacheValues[keys.Length + 1] = (long) Expiration.TotalMilliseconds; + cacheValues = AppendAdditionalValues(cacheValues); + cancellationToken.ThrowIfCancellationRequested(); + await (Database.ScriptEvaluateAsync(PutManyScript, cacheKeys, cacheValues)).ConfigureAwait(false); + } + } + + /// + /// Removes the key from Redis. + /// + /// The key to remove. + /// A cancellation token that can be used to cancel the work + public virtual Task RemoveAsync(object key, CancellationToken cancellationToken) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalRemoveAsync(); + async Task InternalRemoveAsync() + { + + var cacheKey = GetCacheKey(key); + Log.Debug("Removing object with key: '{0}'.", cacheKey); + if (string.IsNullOrEmpty(RemoveScript)) + { + cancellationToken.ThrowIfCancellationRequested(); + return await (Database.KeyDeleteAsync(cacheKey)).ConfigureAwait(false); + } + + var keys = AppendAdditionalKeys(new RedisKey[] {cacheKey}); + var values = GetAdditionalValues(); + cancellationToken.ThrowIfCancellationRequested(); + var results = (RedisValue[]) await (Database.ScriptEvaluateAsync(RemoveScript, keys, values)).ConfigureAwait(false); + return (bool) results[0]; + } + } + + /// + /// Locks the key. + /// + /// The key to lock. + /// A cancellation token that can be used to cancel the work + /// The value used to lock the key. + public virtual Task LockAsync(object key, CancellationToken cancellationToken) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + + var cacheKey = GetCacheKey(key); + Log.Debug("Locking object with key: '{0}'.", cacheKey); + return _keyLocker.LockAsync(cacheKey, LockScript, GetAdditionalKeys(), GetAdditionalValues(), cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + /// Locks many keys at once. + /// + /// The keys to lock. + /// A cancellation token that can be used to cancel the work + /// The value used to lock the keys. + public virtual Task LockManyAsync(object[] keys, CancellationToken cancellationToken) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + + if (string.IsNullOrEmpty(LockManyScript)) + { + throw new NotSupportedException($"{nameof(LockManyAsync)} operation is not supported."); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + + Log.Debug("Locking {0} objects...", keys.Length); + var cacheKeys = new string[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + cacheKeys[i] = GetCacheKey(keys[i]); + Log.Debug("Locking object with key: '{0}'.", cacheKeys[i]); + } + + return _keyLocker.LockManyAsync(cacheKeys, LockManyScript, GetAdditionalKeys(), GetAdditionalValues(), cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + /// Unlocks the key. + /// + /// The key to unlock. + /// The value used to lock the key. + /// A cancellation token that can be used to cancel the work + /// Whether the key was unlocked + public virtual Task UnlockAsync(object key, string lockValue, CancellationToken cancellationToken) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalUnlockAsync(); + async Task InternalUnlockAsync() + { + + var cacheKey = GetCacheKey(key); + Log.Debug("Unlocking object with key: '{0}'.", cacheKey); + var unlocked = await (_keyLocker.UnlockAsync(cacheKey, lockValue, UnlockScript, GetAdditionalKeys(), GetAdditionalValues(), cancellationToken)).ConfigureAwait(false); + Log.Debug("Unlock key '{0}' result: {1}", cacheKey, unlocked); + return unlocked; + } + } + + /// + /// Unlocks many keys at once. + /// + /// The keys to unlock. + /// The value used to lock the keys. + /// A cancellation token that can be used to cancel the work + /// The number of unlocked keys. + public virtual Task UnlockManyAsync(object[] keys, string lockValue, CancellationToken cancellationToken) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + + if (string.IsNullOrEmpty(UnlockManyScript)) + { + throw new NotSupportedException($"{nameof(UnlockManyAsync)} operation is not supported."); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalUnlockManyAsync(); + async Task InternalUnlockManyAsync() + { + + Log.Debug("Unlocking {0} objects...", keys.Length); + var cacheKeys = new string[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + cacheKeys[i] = GetCacheKey(keys[i]); + Log.Debug("Unlocking object with key: '{0}'.", cacheKeys[i]); + } + + var unlockedKeys = + await (_keyLocker.UnlockManyAsync(cacheKeys, lockValue, UnlockManyScript, GetAdditionalKeys(), GetAdditionalValues(), cancellationToken)).ConfigureAwait(false); + if (Log.IsDebugEnabled()) + { + Log.Debug("Number of unlocked objects with keys ({0}): {1}", string.Join(",", cacheKeys.Select(o => $"'{o}'")), + unlockedKeys); + } + + return unlockedKeys; + } + } + + /// + /// Clears all the keys from the region. + /// + /// A cancellation token that can be used to cancel the work + public abstract Task ClearAsync(CancellationToken cancellationToken); + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/DefaultRegionStrategy.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/DefaultRegionStrategy.cs new file mode 100644 index 00000000..e46f0add --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/DefaultRegionStrategy.cs @@ -0,0 +1,226 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; +using StackExchange.Redis; +using static NHibernate.Caches.StackExchangeRedis.ConfigurationHelper; + +namespace NHibernate.Caches.StackExchangeRedis +{ + using System.Threading.Tasks; + using System.Threading; + public partial class DefaultRegionStrategy : AbstractRegionStrategy + { + + /// + public override async Task GetAsync(object key, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return await (base.GetAsync(key, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + if (Log.IsDebugEnabled()) + { + Log.Debug("Retry to fetch the object with key: '{0}'", CurrentVersion, GetCacheKey(key)); + } + return await (base.GetAsync(key, cancellationToken)).ConfigureAwait(false); + } + } + + /// + public override async Task GetManyAsync(object[] keys, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return await (base.GetManyAsync(keys, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + if (Log.IsDebugEnabled()) + { + Log.Debug("Retry to fetch objects with keys: {0}", + CurrentVersion, + string.Join(",", keys.Select(o => $"'{GetCacheKey(o)}'"))); + } + return await (base.GetManyAsync(keys, cancellationToken)).ConfigureAwait(false); + } + } + + /// + public override async Task LockAsync(object key, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return await (base.LockAsync(key, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + if (Log.IsDebugEnabled()) + { + Log.Debug("Retry to lock the object with key: '{0}'", CurrentVersion, GetCacheKey(key)); + } + return await (base.LockAsync(key, cancellationToken)).ConfigureAwait(false); + } + } + + /// + public override async Task LockManyAsync(object[] keys, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return await (base.LockManyAsync(keys, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + if (Log.IsDebugEnabled()) + { + Log.Debug("Retry to lock objects with keys: {0}", + CurrentVersion, + string.Join(",", keys.Select(o => $"'{GetCacheKey(o)}'"))); + } + return await (base.LockManyAsync(keys, cancellationToken)).ConfigureAwait(false); + } + } + + /// + public override async Task PutAsync(object key, object value, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + await (base.PutAsync(key, value, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + // Here we don't know if the operation was executed after as successful lock, so + // the easiest solution is to skip the operation + } + } + + /// + public override async Task PutManyAsync(object[] keys, object[] values, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + await (base.PutManyAsync(keys, values, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + // Here we don't know if the operation was executed after as successful lock, so + // the easiest solution is to skip the operation + } + } + + /// + public override async Task RemoveAsync(object key, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return await (base.RemoveAsync(key, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + // There is no point removing the key in the new version. + return false; + } + } + + /// + public override async Task UnlockAsync(object key, string lockValue, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return await (base.UnlockAsync(key, lockValue, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + // If the lock was acquired in the old version we are unable to unlock the key. + return false; + } + } + + /// + public override async Task UnlockManyAsync(object[] keys, string lockValue, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return await (base.UnlockManyAsync(keys, lockValue, cancellationToken)).ConfigureAwait(false); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + cancellationToken.ThrowIfCancellationRequested(); + await (InitializeVersionAsync()).ConfigureAwait(false); + // If the lock was acquired in the old version we are unable to unlock the keys. + return 0; + } + } + + /// + public override async Task ClearAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + Log.Debug("Clearing region: '{0}'.", RegionKey); + cancellationToken.ThrowIfCancellationRequested(); + var results = (RedisValue[]) await (Database.ScriptEvaluateAsync(UpdateVersionLuaScript, + _regionKeyArray, _maxVersionNumber)).ConfigureAwait(false); + var version = results[0]; + UpdateVersion(version); + if (_usePubSub) + { + cancellationToken.ThrowIfCancellationRequested(); + await (ConnectionMultiplexer.GetSubscriber().PublishAsync(RegionKey, version)).ConfigureAwait(false); + } + } + + private async Task InitializeVersionAsync() + { + var results = (RedisValue[]) await (Database.ScriptEvaluateAsync(InitializeVersionLuaScript, _regionKeyArray)).ConfigureAwait(false); + var version = results[0]; + UpdateVersion(version); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/FastRegionStrategy.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/FastRegionStrategy.cs new file mode 100644 index 00000000..0d09b5f9 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/FastRegionStrategy.cs @@ -0,0 +1,29 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + using System.Threading.Tasks; + using System.Threading; + public partial class FastRegionStrategy : AbstractRegionStrategy + { + + /// + public override Task ClearAsync(CancellationToken cancellationToken) + { + throw new NotSupportedException( + $"{nameof(ClearAsync)} operation is not supported, if it cannot be avoided use {nameof(DefaultRegionStrategy)}."); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/NHibernateTextWriter.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/NHibernateTextWriter.cs new file mode 100644 index 00000000..3b932cd5 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/NHibernateTextWriter.cs @@ -0,0 +1,59 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.IO; +using System.Text; + +namespace NHibernate.Caches.StackExchangeRedis +{ + using System.Threading.Tasks; + internal partial class NHibernateTextWriter : TextWriter + { + + public override Task WriteAsync(string value) + { + try + { + Write(value); + return Task.CompletedTask; + } + catch (System.Exception ex) + { + return Task.FromException(ex); + } + } + + public override Task WriteLineAsync(string value) + { + try + { + WriteLine(value); + return Task.CompletedTask; + } + catch (System.Exception ex) + { + return Task.FromException(ex); + } + } + + public override Task WriteLineAsync() + { + try + { + WriteLine(); + return Task.CompletedTask; + } + catch (System.Exception ex) + { + return Task.FromException(ex); + } + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/RedisCache.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/RedisCache.cs new file mode 100644 index 00000000..50440044 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/RedisCache.cs @@ -0,0 +1,128 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NHibernate.Cache; + +namespace NHibernate.Caches.StackExchangeRedis +{ + using System.Threading.Tasks; + using System.Threading; + public partial class RedisCache : CacheBase + { + + /// + public override Task GetAsync(object key, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return RegionStrategy.GetAsync(key, cancellationToken); + } + + /// + public override Task GetManyAsync(object[] keys, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return RegionStrategy.GetManyAsync(keys, cancellationToken); + } + + /// + public override Task PutAsync(object key, object value, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return RegionStrategy.PutAsync(key, value, cancellationToken); + } + + /// + public override Task PutManyAsync(object[] keys, object[] values, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return RegionStrategy.PutManyAsync(keys, values, cancellationToken); + } + + /// + public override Task RemoveAsync(object key, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return RegionStrategy.RemoveAsync(key, cancellationToken); + } + + /// + public override Task ClearAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return RegionStrategy.ClearAsync(cancellationToken); + } + + /// + public override async Task LockAsync(object key, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return await (RegionStrategy.LockAsync(key, cancellationToken)).ConfigureAwait(false); + } + + /// + public override async Task LockManyAsync(object[] keys, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return await (RegionStrategy.LockManyAsync(keys, cancellationToken)).ConfigureAwait(false); + } + + /// + public override Task UnlockAsync(object key, object lockValue, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return RegionStrategy.UnlockAsync(key, (string)lockValue, cancellationToken); + } + catch (System.Exception ex) + { + return Task.FromException(ex); + } + } + + /// + public override Task UnlockManyAsync(object[] keys, object lockValue, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return RegionStrategy.UnlockManyAsync(keys, (string) lockValue, cancellationToken); + } + catch (System.Exception ex) + { + return Task.FromException(ex); + } + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/RedisKeyLocker.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/RedisKeyLocker.cs new file mode 100644 index 00000000..232108bf --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Async/RedisKeyLocker.cs @@ -0,0 +1,224 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Linq; +using NHibernate.Cache; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + using System.Threading.Tasks; + using System.Threading; + internal partial class RedisKeyLocker + { + + /// + /// Tries to lock the given key. + /// + /// The key to lock. + /// The lua script to lock the key. + /// The extra keys that will be provided to the + /// The extra values that will be provided to the + /// A cancellation token that can be used to cancel the work + /// The lock value used to lock the key. + /// Thrown if the lock was not acquired. + public Task LockAsync(string key, string luaScript, RedisKey[] extraKeys, RedisValue[] extraValues, CancellationToken cancellationToken) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + var lockKey = $"{key}{_lockKeySuffix}"; + string Context() => lockKey; + + return _retryPolicy.ExecuteAsync(async () => + { + var lockValue = _lockValueProvider.GetValue(); + if (!string.IsNullOrEmpty(luaScript)) + { + var keys = new RedisKey[] {lockKey}; + if (extraKeys != null) + { + keys = keys.Concat(extraKeys).ToArray(); + } + var values = new RedisValue[] {lockValue, (long) _lockTimeout.TotalMilliseconds}; + if (extraValues != null) + { + values = values.Concat(extraValues).ToArray(); + } + var result = (RedisValue[]) await (_database.ScriptEvaluateAsync(luaScript, keys, values)).ConfigureAwait(false); + if ((bool) result[0]) + { + return lockValue; + } + } + else if (await (_database.LockTakeAsync(lockKey, lockValue, _lockTimeout)).ConfigureAwait(false)) + { + return lockValue; + } + + return null; // retry + }, Context, cancellationToken); + } + + /// + /// Tries to lock the given keys. + /// + /// The keys to lock. + /// The lua script to lock the keys. + /// The extra keys that will be provided to the + /// The extra values that will be provided to the + /// A cancellation token that can be used to cancel the work + /// The lock value used to lock the keys. + /// Thrown if the lock was not acquired. + public Task LockManyAsync(string[] keys, string luaScript, RedisKey[] extraKeys, RedisValue[] extraValues, CancellationToken cancellationToken) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + if (luaScript == null) + { + throw new ArgumentNullException(nameof(luaScript)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + string Context() => string.Join(",", keys.Select(o => $"{o}{_lockKeySuffix}")); + + return _retryPolicy.ExecuteAsync(async () => + { + var lockKeys = new RedisKey[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + lockKeys[i] = $"{keys[i]}{_lockKeySuffix}"; + } + var lockValue = _lockValueProvider.GetValue(); + if (extraKeys != null) + { + lockKeys = lockKeys.Concat(extraKeys).ToArray(); + } + var values = new RedisValue[] {lockValue, (long) _lockTimeout.TotalMilliseconds}; + if (extraValues != null) + { + values = values.Concat(extraValues).ToArray(); + } + var result = (RedisValue[]) await (_database.ScriptEvaluateAsync(luaScript, lockKeys, values)).ConfigureAwait(false); + if ((bool) result[0]) + { + return lockValue; + } + + return null; // retry + }, Context, cancellationToken); + } + + /// + /// Tries to unlock the given key. + /// + /// The key to unlock. + /// The value that was used to lock the key. + /// The lua script to unlock the key. + /// The extra keys that will be provided to the + /// The extra values that will be provided to the + /// A cancellation token that can be used to cancel the work + /// Whether the key was unlocked. + public Task UnlockAsync(string key, string lockValue, string luaScript, RedisKey[] extraKeys, RedisValue[] extraValues, CancellationToken cancellationToken) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalUnlockAsync(); + async Task InternalUnlockAsync() + { + var lockKey = $"{key}{_lockKeySuffix}"; + if (string.IsNullOrEmpty(luaScript)) + { + cancellationToken.ThrowIfCancellationRequested(); + return await (_database.LockReleaseAsync(lockKey, lockValue)).ConfigureAwait(false); + } + var keys = new RedisKey[] {lockKey}; + if (extraKeys != null) + { + keys = keys.Concat(extraKeys).ToArray(); + } + var values = new RedisValue[] {lockValue}; + if (extraValues != null) + { + values = values.Concat(extraValues).ToArray(); + } + cancellationToken.ThrowIfCancellationRequested(); + + var result = (RedisValue[]) await (_database.ScriptEvaluateAsync(luaScript, keys, values)).ConfigureAwait(false); + return (bool) result[0]; + } + } + + /// + /// Tries to unlock the given keys. + /// + /// The keys to unlock. + /// The value that was used to lock the keys. + /// The lua script to unlock the keys. + /// The extra keys that will be provided to the + /// The extra values that will be provided to the + /// A cancellation token that can be used to cancel the work + /// How many keys were unlocked. + public Task UnlockManyAsync(string[] keys, string lockValue, string luaScript, RedisKey[] extraKeys, RedisValue[] extraValues, CancellationToken cancellationToken) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + if (luaScript == null) + { + throw new ArgumentNullException(nameof(luaScript)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalUnlockManyAsync(); + async Task InternalUnlockManyAsync() + { + + var lockKeys = new RedisKey[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + lockKeys[i] = $"{keys[i]}{_lockKeySuffix}"; + } + if (extraKeys != null) + { + lockKeys = lockKeys.Concat(extraKeys).ToArray(); + } + var values = new RedisValue[] {lockValue}; + if (extraValues != null) + { + values = values.Concat(extraValues).ToArray(); + } + cancellationToken.ThrowIfCancellationRequested(); + + var result = (RedisValue[]) await (_database.ScriptEvaluateAsync(luaScript, lockKeys, values)).ConfigureAwait(false); + return (int) result[0]; + } + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/CacheConfig.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/CacheConfig.cs new file mode 100644 index 00000000..e67d01e6 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/CacheConfig.cs @@ -0,0 +1,29 @@ +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Cache configuration properties. + /// + public class CacheConfig + { + /// + /// Build a cache configuration. + /// + /// The redis configuration + /// The configured cache regions. + public CacheConfig(string configuration, RegionConfig[] regions) + { + Regions = regions; + Configuration = configuration; + } + + /// + /// The configured cache regions. + /// + public RegionConfig[] Regions { get; } + + /// + /// The StackExchange.Redis configuration string. + /// + public string Configuration { get; } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ConfigurationHelper.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ConfigurationHelper.cs new file mode 100644 index 00000000..957f590e --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ConfigurationHelper.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using NHibernate.Bytecode; +using NHibernate.Util; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Various methods to easier retrieve the configuration values. + /// + internal static class ConfigurationHelper + { + public static string GetString(string key, IDictionary properties, string defaultValue) + { + if (properties == null) + { + return defaultValue; + } + + return properties.TryGetValue(key, out var value) ? value : defaultValue; + } + + public static bool GetBoolean(string key, IDictionary properties, bool defaultValue) + { + if (properties == null) + { + return defaultValue; + } + + return properties.TryGetValue(key, out var value) ? Convert.ToBoolean(value) : defaultValue; + } + + public static int GetInteger(string key, IDictionary properties, int defaultValue) + { + if (properties == null) + { + return defaultValue; + } + return properties.TryGetValue(key, out var value) ? Convert.ToInt32(value) : defaultValue; + } + + public static TimeSpan GetTimeSpanFromSeconds(string key, IDictionary properties, TimeSpan defaultValue) + { + if (properties == null) + { + return defaultValue; + } + + var seconds = properties.TryGetValue(key, out var value) + ? Convert.ToInt64(value) + : (long) defaultValue.TotalSeconds; + return TimeSpan.FromSeconds(seconds); + } + + public static TimeSpan GetTimeSpanFromMilliseconds(string key, IDictionary properties, TimeSpan defaultValue) + { + if (properties == null) + { + return defaultValue; + } + + var milliseconds = properties.TryGetValue(key, out var value) + ? Convert.ToInt64(value) + : (long) defaultValue.TotalMilliseconds; + return TimeSpan.FromMilliseconds(milliseconds); + } + + public static System.Type GetSystemType(string key, IDictionary properties, System.Type defaultValue) + { + var typeName = GetString(key, properties, null); + return typeName == null ? defaultValue : ReflectHelper.ClassForName(typeName); + } + + public static TType GetInstance(string key, IDictionary properties, TType defaultValue, + INHibernateLogger logger) + { + var objectsFactory = Cfg.Environment.ObjectsFactory; + var className = GetString(key, properties, null); + System.Type type = null; + try + { + if (className != null) + { + type = ReflectHelper.ClassForName(className); + return (TType) objectsFactory.CreateInstance(type); + } + + // Try to get the instance from the base type if the user provided a custom IObjectsFactory + if (!(objectsFactory is ActivatorObjectsFactory)) + { + try + { + return (TType) objectsFactory.CreateInstance(typeof(TType)); + } + catch (Exception e) + { + // The user most likely did not register the TType + logger.Debug( + "Failed to create an instance of type '{0}' by using IObjectsFactory, most probably was not registered. Exception: {1}", + typeof(TType), e); + } + } + + return defaultValue; + } + catch (Exception e) + { + throw new HibernateException( + $"Could not instantiate {typeof(TType).Name}: {type?.AssemblyQualifiedName ?? className}", e); + } + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheLockRetryDelayProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheLockRetryDelayProvider.cs new file mode 100644 index 00000000..ab6884cc --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheLockRetryDelayProvider.cs @@ -0,0 +1,18 @@ +using System; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + public class DefaultCacheLockRetryDelayProvider : ICacheLockRetryDelayProvider + { + private readonly Random _random = new Random(); + + /// + public TimeSpan GetValue(TimeSpan minDelay, TimeSpan maxDelay) + { + var delay = _random.NextDouble() * (maxDelay.TotalMilliseconds - minDelay.TotalMilliseconds) + + minDelay.TotalMilliseconds; + return TimeSpan.FromMilliseconds(delay); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheLockValueProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheLockValueProvider.cs new file mode 100644 index 00000000..d073ed88 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheLockValueProvider.cs @@ -0,0 +1,14 @@ +using System; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + public class DefaultCacheLockValueProvider : ICacheLockValueProvider + { + /// + public string GetValue() + { + return Guid.NewGuid().ToString(); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheRegionStrategyFactory.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheRegionStrategyFactory.cs new file mode 100644 index 00000000..b1a28666 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultCacheRegionStrategyFactory.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using NHibernate.Cache; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + public class DefaultCacheRegionStrategyFactory : ICacheRegionStrategyFactory + { + /// + public virtual AbstractRegionStrategy Create(IConnectionMultiplexer connectionMultiplexer, + RedisCacheRegionConfiguration configuration, IDictionary properties) + { + if (configuration.RegionStrategy == typeof(DefaultRegionStrategy)) + { + return new DefaultRegionStrategy(connectionMultiplexer, configuration, properties); + } + if (configuration.RegionStrategy == typeof(FastRegionStrategy)) + { + return new FastRegionStrategy(connectionMultiplexer, configuration, properties); + } + + throw new CacheException( + $"{configuration.RegionStrategy} is not supported by {GetType()}, register " + + $"a custom {typeof(ICacheRegionStrategyFactory)} or use a supported one."); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultConnectionMultiplexerProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultConnectionMultiplexerProvider.cs new file mode 100644 index 00000000..53c7ba3c --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultConnectionMultiplexerProvider.cs @@ -0,0 +1,19 @@ +using System.IO; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + public class DefaultConnectionMultiplexerProvider : IConnectionMultiplexerProvider + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(DefaultConnectionMultiplexerProvider)); + + /// + public IConnectionMultiplexer Get(string configuration) + { + TextWriter textWriter = Log.IsDebugEnabled() ? new NHibernateTextWriter(Log) : null; + var connectionMultiplexer = ConnectionMultiplexer.Connect(configuration, textWriter); + return connectionMultiplexer; + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultDatabaseProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultDatabaseProvider.cs new file mode 100644 index 00000000..d57617b0 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultDatabaseProvider.cs @@ -0,0 +1,14 @@ +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + public class DefaultDatabaseProvider : IDatabaseProvider + { + /// + public IDatabase Get(IConnectionMultiplexer connectionMultiplexer, int database) + { + return connectionMultiplexer.GetDatabase(database); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultRegionStrategy.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultRegionStrategy.cs new file mode 100644 index 00000000..9e18fc23 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/DefaultRegionStrategy.cs @@ -0,0 +1,329 @@ +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; +using StackExchange.Redis; +using static NHibernate.Caches.StackExchangeRedis.ConfigurationHelper; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// The default region strategy. This strategy uses a special key that contains the region current version number which is appended + /// after the region prefix. Each time a clear operation is performed the version number is increased and an event is send to all + /// clients so that they can update their local versions. Even if the event was not sent to all clients, each operation has a + /// version check in order to prevent working with stale data. + /// + public partial class DefaultRegionStrategy : AbstractRegionStrategy + { + private const string InvalidVersionMessage = "Invalid version"; + private static readonly string UpdateVersionLuaScript; + private static readonly string InitializeVersionLuaScript; + private static readonly string GetLuaScript; + private static readonly string GetManyLuaScript; + private static readonly string PutLuaScript; + private static readonly string PutManyLuaScript; + private static readonly string RemoveLuaScript; + private static readonly string LockLuaScript; + private static readonly string LockManyLuaScript; + private static readonly string UnlockLuaScript; + private static readonly string UnlockManyLuaScript; + + static DefaultRegionStrategy() + { + UpdateVersionLuaScript = LuaScriptProvider.GetScript("UpdateVersion"); + InitializeVersionLuaScript = LuaScriptProvider.GetScript("InitializeVersion"); + // For each operation we have to prepend the check version script + const string checkVersion = "CheckVersion"; + GetLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(Get)); + GetManyLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(GetMany)); + PutLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(Put)); + PutManyLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(PutMany)); + RemoveLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(Remove)); + LockLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(Lock)); + LockManyLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(LockMany)); + UnlockLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(Unlock)); + UnlockManyLuaScript = LuaScriptProvider.GetConcatenatedScript(checkVersion, nameof(UnlockMany)); + } + + + private readonly RedisKey[] _regionKeyArray; + private readonly RedisValue[] _maxVersionNumber; + private RedisValue _currentVersion; + private RedisValue[] _currentVersionArray; + private readonly bool _usePubSub; + + /// + /// Default constructor. + /// + public DefaultRegionStrategy(IConnectionMultiplexer connectionMultiplexer, + RedisCacheRegionConfiguration configuration, IDictionary properties) + : base(connectionMultiplexer, configuration, properties) + { + var maxVersion = GetInteger("cache.region_strategy.default.max_allowed_version", properties, 1000); + Log.Debug("Max allowed version for region {0}: {1}", RegionName, maxVersion); + + _usePubSub = GetBoolean("cache.region_strategy.default.use_pubsub", properties, true); + Log.Debug("Use pubsub for region {0}: {1}", RegionName, _usePubSub); + + _regionKeyArray = new RedisKey[] {RegionKey}; + _maxVersionNumber = new RedisValue[] {maxVersion}; + InitializeVersion(); + + if (_usePubSub) + { + ConnectionMultiplexer.GetSubscriber().SubscribeAsync(RegionKey, (channel, value) => + { + UpdateVersion(value); + }); + } + } + + /// + /// The version number that is currently used to retrieve/store keys. + /// + public long CurrentVersion => (long) _currentVersion; + + /// + protected override string GetScript => GetLuaScript; + + /// + protected override string GetManyScript => GetManyLuaScript; + + /// + protected override string PutScript => PutLuaScript; + + /// + protected override string PutManyScript => PutManyLuaScript; + + /// + protected override string RemoveScript => RemoveLuaScript; + + /// + protected override string LockScript => LockLuaScript; + + /// + protected override string LockManyScript => LockManyLuaScript; + + /// + protected override string UnlockScript => UnlockLuaScript; + + /// + protected override string UnlockManyScript => UnlockManyLuaScript; + + /// + public override object Get(object key) + { + try + { + return base.Get(key); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + if (Log.IsDebugEnabled()) + { + Log.Debug("Retry to fetch the object with key: '{0}'", CurrentVersion, GetCacheKey(key)); + } + return base.Get(key); + } + } + + /// + public override object[] GetMany(object[] keys) + { + try + { + return base.GetMany(keys); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + if (Log.IsDebugEnabled()) + { + Log.Debug("Retry to fetch objects with keys: {0}", + CurrentVersion, + string.Join(",", keys.Select(o => $"'{GetCacheKey(o)}'"))); + } + return base.GetMany(keys); + } + } + + /// + public override string Lock(object key) + { + try + { + return base.Lock(key); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + if (Log.IsDebugEnabled()) + { + Log.Debug("Retry to lock the object with key: '{0}'", CurrentVersion, GetCacheKey(key)); + } + return base.Lock(key); + } + } + + /// + public override string LockMany(object[] keys) + { + try + { + return base.LockMany(keys); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + if (Log.IsDebugEnabled()) + { + Log.Debug("Retry to lock objects with keys: {0}", + CurrentVersion, + string.Join(",", keys.Select(o => $"'{GetCacheKey(o)}'"))); + } + return base.LockMany(keys); + } + } + + /// + public override void Put(object key, object value) + { + try + { + base.Put(key, value); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + // Here we don't know if the operation was executed after as successful lock, so + // the easiest solution is to skip the operation + } + } + + /// + public override void PutMany(object[] keys, object[] values) + { + try + { + base.PutMany(keys, values); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + // Here we don't know if the operation was executed after as successful lock, so + // the easiest solution is to skip the operation + } + } + + /// + public override bool Remove(object key) + { + try + { + return base.Remove(key); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + // There is no point removing the key in the new version. + return false; + } + } + + /// + public override bool Unlock(object key, string lockValue) + { + try + { + return base.Unlock(key, lockValue); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + // If the lock was acquired in the old version we are unable to unlock the key. + return false; + } + } + + /// + public override int UnlockMany(object[] keys, string lockValue) + { + try + { + return base.UnlockMany(keys, lockValue); + } + catch (RedisServerException e) when (e.Message == InvalidVersionMessage) + { + Log.Debug("Version '{0}' is not valid anymore, updating version...", CurrentVersion); + InitializeVersion(); + // If the lock was acquired in the old version we are unable to unlock the keys. + return 0; + } + } + + /// + public override void Clear() + { + Log.Debug("Clearing region: '{0}'.", RegionKey); + var results = (RedisValue[]) Database.ScriptEvaluate(UpdateVersionLuaScript, + _regionKeyArray, _maxVersionNumber); + var version = results[0]; + UpdateVersion(version); + if (_usePubSub) + { + ConnectionMultiplexer.GetSubscriber().Publish(RegionKey, version); + } + } + + /// + public override void Validate() + { + if (!ExpirationEnabled) + { + throw new CacheException($"Expiration must be greater than zero for cache region: '{RegionName}'"); + } + } + + /// + protected override RedisKey[] GetAdditionalKeys() + { + return _regionKeyArray; + } + + /// + protected override RedisValue[] GetAdditionalValues() + { + return _currentVersionArray; + } + + /// + protected override string GetCacheKey(object value) + { + return AppendHashcode + ? string.Concat("{", RegionKey, "}-", _currentVersion, ":", value.ToString(), "@", value.GetHashCode()) + : string.Concat("{", RegionKey, "}-", _currentVersion, ":", value.ToString()); + } + + private void InitializeVersion() + { + var results = (RedisValue[]) Database.ScriptEvaluate(InitializeVersionLuaScript, _regionKeyArray); + var version = results[0]; + UpdateVersion(version); + } + + private void UpdateVersion(RedisValue version) + { + Log.Debug("Updating version from '{0}' to '{1}'.", CurrentVersion, version); + _currentVersion = version; + _currentVersionArray = new[] {version}; + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/FastRegionStrategy.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/FastRegionStrategy.cs new file mode 100644 index 00000000..072fbfbf --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/FastRegionStrategy.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// A region strategy that have very simple read/write operations but does not support + /// operation. + /// + public partial class FastRegionStrategy : AbstractRegionStrategy + { + private static readonly string SlidingGetLuaScript; + private static readonly string SlidingGetManyLuaScript; + private static readonly string ExpirationPutManyLuaScript; + private static readonly string LockManyLuaScript; + private static readonly string UnlockLuaScript; + private static readonly string UnlockManyLuaScript; + + static FastRegionStrategy() + { + SlidingGetLuaScript = LuaScriptProvider.GetScript("SlidingGet"); + SlidingGetManyLuaScript = LuaScriptProvider.GetScript("SlidingGetMany"); + ExpirationPutManyLuaScript = LuaScriptProvider.GetScript("ExpirationPutMany"); + LockManyLuaScript = LuaScriptProvider.GetScript(nameof(LockMany)); + UnlockLuaScript = LuaScriptProvider.GetScript(nameof(Unlock)); + UnlockManyLuaScript = LuaScriptProvider.GetScript(nameof(UnlockMany)); + } + + + /// + /// Default constructor. + /// + public FastRegionStrategy(IConnectionMultiplexer connectionMultiplexer, + RedisCacheRegionConfiguration configuration, IDictionary properties) + : base(connectionMultiplexer, configuration, properties) + { + if (!ExpirationEnabled) + { + return; + } + PutManyScript = ExpirationPutManyLuaScript; + if (UseSlidingExpiration) + { + GetScript = SlidingGetLuaScript; + GetManyScript = SlidingGetManyLuaScript; + } + } + + /// + protected override string GetScript { get; } + + /// + protected override string GetManyScript { get; } + + /// + protected override string PutManyScript { get; } + + /// + protected override string LockManyScript => LockManyLuaScript; + + /// + protected override string UnlockScript => UnlockLuaScript; + + /// + protected override string UnlockManyScript => UnlockManyLuaScript; + + /// + public override void Clear() + { + throw new NotSupportedException( + $"{nameof(Clear)} operation is not supported, if it cannot be avoided use {nameof(DefaultRegionStrategy)}."); + } + + /// + public override void Validate() + { + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheLockRetryDelayProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheLockRetryDelayProvider.cs new file mode 100644 index 00000000..37b81ebe --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheLockRetryDelayProvider.cs @@ -0,0 +1,18 @@ +using System; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Defines a method to return a to be waited before the next lock attempt. + /// + public interface ICacheLockRetryDelayProvider + { + /// + /// Get a delay value between two values. + /// + /// The minimum delay value. + /// The maximum delay value. + /// + TimeSpan GetValue(TimeSpan minDelay, TimeSpan maxDelay); + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheLockValueProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheLockValueProvider.cs new file mode 100644 index 00000000..6931704a --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheLockValueProvider.cs @@ -0,0 +1,15 @@ +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Defines a method to get a unique value that will be used as a value when locking keys in + /// order to identify which instance locked the key. + /// + public interface ICacheLockValueProvider + { + /// + /// Gets a unique value that will be used for locking keys. + /// + /// A unique value. + string GetValue(); + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheRegionStrategyFactory.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheRegionStrategyFactory.cs new file mode 100644 index 00000000..d2869b94 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/ICacheRegionStrategyFactory.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Defines a factory to create concrete instances. + /// + public interface ICacheRegionStrategyFactory + { + /// + /// Creates a concrete instance. + /// + /// The connection to be used. + /// The region configuration. + /// The properties from NHibernate configuration. + /// + AbstractRegionStrategy Create(IConnectionMultiplexer connectionMultiplexer, + RedisCacheRegionConfiguration configuration, IDictionary properties); + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/IConnectionMultiplexerProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/IConnectionMultiplexerProvider.cs new file mode 100644 index 00000000..ff8f49dc --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/IConnectionMultiplexerProvider.cs @@ -0,0 +1,17 @@ +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Defines a method to provide an instance. + /// + public interface IConnectionMultiplexerProvider + { + /// + /// Provide the for the StackExchange.Redis configuration string. + /// + /// The StackExchange.Redis configuration string + /// The instance. + IConnectionMultiplexer Get(string configuration); + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/IDatabaseProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/IDatabaseProvider.cs new file mode 100644 index 00000000..76a08d76 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/IDatabaseProvider.cs @@ -0,0 +1,18 @@ +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Defines a method to provide an instance. + /// + public interface IDatabaseProvider + { + /// + /// Provide the for the given and database index. + /// + /// The connection multiplexer. + /// The database index. + /// The instance. + IDatabase Get(IConnectionMultiplexer connectionMultiplexer, int database); + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/CheckVersion.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/CheckVersion.lua new file mode 100644 index 00000000..338af90d --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/CheckVersion.lua @@ -0,0 +1,4 @@ +local version = redis.call('get', KEYS[#KEYS]) +if version ~= ARGV[#ARGV] then + return redis.error_reply('Invalid version') +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Get.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Get.lua new file mode 100644 index 00000000..f7326f76 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Get.lua @@ -0,0 +1,5 @@ +local value = redis.call('get', KEYS[1]) +if value ~= nil and ARGV[1] == '1' then + redis.call('pexpire', KEYS[1], ARGV[2]) +end +return value diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/GetMany.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/GetMany.lua new file mode 100644 index 00000000..d6986c6e --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/GetMany.lua @@ -0,0 +1,11 @@ +local values = {{}} +local sliding = ARGV[#ARGV-2] +local expirationMs = ARGV[#ARGV-1] +for i=1,#KEYS-1 do + local value = redis.call('get', KEYS[i]) + if value ~= nil and sliding == '1' then + redis.call('pexpire', KEYS[i], expirationMs) + end + values[i] = value +end +return values diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/InitializeVersion.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/InitializeVersion.lua new file mode 100644 index 00000000..73bd5145 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/InitializeVersion.lua @@ -0,0 +1,6 @@ +if redis.call('exists', KEYS[1]) == 1 then + return redis.call('get', KEYS[1]) +else + redis.call('set', KEYS[1], 1) + return 1 +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Lock.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Lock.lua new file mode 100644 index 00000000..ed59efed --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Lock.lua @@ -0,0 +1,5 @@ +if redis.call('set', KEYS[1], ARGV[1], 'nx', 'px', ARGV[2]) == false then + return 0 +else + return 1 +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/LockMany.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/LockMany.lua new file mode 100644 index 00000000..86e10255 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/LockMany.lua @@ -0,0 +1,22 @@ +local lockValue = ARGV[#ARGV-2] +local expirationMs = ARGV[#ARGV-1] +local lockedKeys = {{}} +local lockedKeyIndex = 1 +local locked = true +for i=1,#KEYS-1 do + if redis.call('set', KEYS[i], lockValue, 'nx', 'px', expirationMs) == false then + locked = 0 + break + else + lockedKeys[lockedKeyIndex] = KEYS[i] + lockedKeyIndex = lockedKeyIndex + 1 + end +end +if locked == true then + return 1 +else + for i=1,#lockedKeys do + redis.call('del', lockedKeys[i]) + end + return 0 +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Put.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Put.lua new file mode 100644 index 00000000..40c9bcd7 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Put.lua @@ -0,0 +1 @@ +return redis.call('set', KEYS[1], ARGV[1], 'px', ARGV[3]) diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/PutMany.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/PutMany.lua new file mode 100644 index 00000000..65657d4e --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/PutMany.lua @@ -0,0 +1,4 @@ +local expirationMs = ARGV[#ARGV-1] +for i=1,#KEYS-1 do + redis.call('set', KEYS[i], ARGV[i], 'px', expirationMs) +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Remove.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Remove.lua new file mode 100644 index 00000000..661b3fcc --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Remove.lua @@ -0,0 +1 @@ +return redis.call('del', KEYS[1]) diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Unlock.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Unlock.lua new file mode 100644 index 00000000..5d64dc4a --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/Unlock.lua @@ -0,0 +1,5 @@ +if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) +else + return 0 +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/UnlockMany.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/UnlockMany.lua new file mode 100644 index 00000000..55e4539e --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/UnlockMany.lua @@ -0,0 +1,8 @@ +local lockValue = ARGV[1] +local removedKeys = 0 +for i=1,#KEYS-1 do + if redis.call('get', KEYS[i]) == lockValue then + removedKeys = removedKeys + redis.call('del', KEYS[i]) + end +end +return removedKeys diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/UpdateVersion.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/UpdateVersion.lua new file mode 100644 index 00000000..29e3cdae --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/DefaultRegionStrategy/UpdateVersion.lua @@ -0,0 +1,6 @@ +local version = redis.call('incr', KEYS[1]) +if version > tonumber(ARGV[1]) then + version = 1 + redis.call('set', KEYS[1], version) +end +return version diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/ExpirationPutMany.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/ExpirationPutMany.lua new file mode 100644 index 00000000..c4e4c9b6 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/ExpirationPutMany.lua @@ -0,0 +1,4 @@ +local expirationMs = ARGV[#ARGV] +for i=1,#KEYS do + redis.call('set', KEYS[i], ARGV[i], 'px', expirationMs) +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/LockMany.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/LockMany.lua new file mode 100644 index 00000000..88f3a108 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/LockMany.lua @@ -0,0 +1,22 @@ +local lockValue = ARGV[#ARGV-1] +local expirationMs = ARGV[#ARGV] +local lockedKeys = {} +local lockedKeyIndex = 1 +local locked = true +for i=1,#KEYS do + if redis.call('set', KEYS[i], lockValue, 'nx', 'px', expirationMs) == false then + locked = false + break + else + lockedKeys[lockedKeyIndex] = KEYS[i] + lockedKeyIndex = lockedKeyIndex + 1 + end +end +if locked == true then + return 1 +else + for i=1,#lockedKeys do + redis.call('del', lockedKeys[i]) + end + return 0 +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/SlidingGet.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/SlidingGet.lua new file mode 100644 index 00000000..6762261a --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/SlidingGet.lua @@ -0,0 +1,5 @@ +local value = redis.call('get', KEYS[1]) +if value ~= nil then + redis.call('pexpire', KEYS[1], ARGV[2]) +end +return value diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/SlidingGetMany.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/SlidingGetMany.lua new file mode 100644 index 00000000..6960b26e --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/SlidingGetMany.lua @@ -0,0 +1,8 @@ +local expirationMs = ARGV[2] +local values = redis.call('MGET', unpack(KEYS)); +for i=1,#KEYS do + if values[i] ~= nil then + redis.call('pexpire', KEYS[i], expirationMs) + end +end +return values diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/Unlock.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/Unlock.lua new file mode 100644 index 00000000..5d64dc4a --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/Unlock.lua @@ -0,0 +1,5 @@ +if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) +else + return 0 +end diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/UnlockMany.lua b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/UnlockMany.lua new file mode 100644 index 00000000..4f5f64e3 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/Lua/FastRegionStrategy/UnlockMany.lua @@ -0,0 +1,8 @@ +local lockValue = ARGV[1] +local removedKeys = 0 +for i=1,#KEYS do + if redis.call('get', KEYS[i]) == lockValue then + removedKeys = removedKeys + redis.call('del', KEYS[i]) + end +end +return removedKeys diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/LuaScriptProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/LuaScriptProvider.cs new file mode 100644 index 00000000..c2727ed5 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/LuaScriptProvider.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Provides the lua scripts of the internal region strategies from the embedded resources. + /// + internal static class LuaScriptProvider + { + // Dictionary> + private static readonly Dictionary> StrategyLuaScripts = + new Dictionary>(); + + static LuaScriptProvider() + { + var assembly = Assembly.GetExecutingAssembly(); + var regex = new Regex(@"Lua\.([\w]+)\.([\w]+)\.lua"); + foreach (var resourceName in assembly.GetManifestResourceNames()) + { + var match = regex.Match(resourceName); + if (!match.Success) + { + continue; + } + if (!StrategyLuaScripts.TryGetValue(match.Groups[1].Value, out var luaScripts)) + { + luaScripts = new Dictionary(); + StrategyLuaScripts.Add(match.Groups[1].Value, luaScripts); + } + + using (var stream = assembly.GetManifestResourceStream(resourceName)) + using (var reader = new StreamReader(stream)) + { + luaScripts.Add(match.Groups[2].Value, reader.ReadToEnd()); + } + } + } + + /// + /// Get the concatenation of multiple lua scripts for the region strategy. + /// + /// The region strategy. + /// The script names to concatenate. + /// The concatenation of multiple lua scripts. + public static string GetConcatenatedScript(params string[] scriptNames) where TRegionStrategy : AbstractRegionStrategy + { + var scriptBuilder = new StringBuilder(); + foreach (var scriptName in scriptNames) + { + scriptBuilder.Append(GetScript(scriptName)); + } + return scriptBuilder.ToString(); + } + + /// + /// Get the lua script for the region strategy. + /// + /// The region strategy. + /// The script name. + /// The lua script. + public static string GetScript(string scriptName) where TRegionStrategy : AbstractRegionStrategy + { + if (!StrategyLuaScripts.TryGetValue(typeof(TRegionStrategy).Name, out var luaScripts)) + { + throw new KeyNotFoundException( + $"There are no embedded scripts for region strategy {typeof(TRegionStrategy).Name}."); + } + if (!luaScripts.TryGetValue(scriptName, out var script)) + { + throw new KeyNotFoundException( + $"There is no embedded script with name {scriptName} for region strategy {typeof(TRegionStrategy).Name}."); + } + return script; + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.csproj b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.csproj new file mode 100644 index 00000000..d83d95d3 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/NHibernate.Caches.StackExchangeRedis.csproj @@ -0,0 +1,38 @@ + + + + NHibernate.Caches.StackExchangeRedis + NHibernate.Caches.StackExchangeRedis + Redis cache provider for NHibernate using StackExchange.Redis. + + net461;netstandard2.0 + True + ..\..\NHibernate.Caches.snk + true + + + NETFX;$(DefineConstants) + + + + + + + + + + + + + + + ./NHibernate.Caches.readme.md + + + ./NHibernate.Caches.license.txt + + + + + + diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/NHibernateTextWriter.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/NHibernateTextWriter.cs new file mode 100644 index 00000000..edfe8338 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/NHibernateTextWriter.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Text; + +namespace NHibernate.Caches.StackExchangeRedis +{ + internal partial class NHibernateTextWriter : TextWriter + { + private readonly INHibernateLogger _logger; + + public NHibernateTextWriter(INHibernateLogger logger) + { + _logger = logger; + } + + public override Encoding Encoding => Encoding.UTF8; + + public override void Write(string value) + { + if (value == null) + { + return; + } + + _logger.Debug(value); + } + + public override void WriteLine(string value) + { + if (value == null) + { + return; + } + _logger.Debug(value); + } + + public override void WriteLine() + { + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCache.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCache.cs new file mode 100644 index 00000000..46b88c30 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCache.cs @@ -0,0 +1,103 @@ +using NHibernate.Cache; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// A cache used to store objects into a Redis cache. + /// + public partial class RedisCache : CacheBase + { + /// + /// Default constructor. + /// + public RedisCache(string regionName, AbstractRegionStrategy regionStrategy) + { + RegionName = regionName; + RegionStrategy = regionStrategy; + Timeout = Timestamper.OneMs * (int) RegionStrategy.LockTimeout.TotalMilliseconds; + } + + /// + public override int Timeout { get; } + + /// + public override string RegionName { get; } + + /// + /// The region strategy used by the cache. + /// + public AbstractRegionStrategy RegionStrategy { get; } + + /// + public override object Get(object key) + { + return RegionStrategy.Get(key); + } + + /// + public override object[] GetMany(object[] keys) + { + return RegionStrategy.GetMany(keys); + } + + /// + public override void Put(object key, object value) + { + RegionStrategy.Put(key, value); + } + + /// + public override void PutMany(object[] keys, object[] values) + { + RegionStrategy.PutMany(keys, values); + } + + /// + public override void Remove(object key) + { + RegionStrategy.Remove(key); + } + + /// + public override void Clear() + { + RegionStrategy.Clear(); + } + + /// + public override void Destroy() + { + // No resources to clean-up. + } + + /// + public override object Lock(object key) + { + return RegionStrategy.Lock(key); + } + + /// + public override object LockMany(object[] keys) + { + return RegionStrategy.LockMany(keys); + } + + /// + public override void Unlock(object key, object lockValue) + { + RegionStrategy.Unlock(key, (string)lockValue); + } + + /// + public override void UnlockMany(object[] keys, object lockValue) + { + RegionStrategy.UnlockMany(keys, (string) lockValue); + } + + /// + public override long NextTimestamp() + { + return Timestamper.Next(); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheConfiguration.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheConfiguration.cs new file mode 100644 index 00000000..989e2177 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheConfiguration.cs @@ -0,0 +1,111 @@ +using System; +using NHibernate.Caches.Common; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Global cache configuration. + /// + public class RedisCacheConfiguration + { + private static readonly CacheSerializerBase DefaultSerializer = new BinaryCacheSerializer(); + private static readonly ICacheRegionStrategyFactory DefaultRegionStrategyFactory = new DefaultCacheRegionStrategyFactory(); + private static readonly IConnectionMultiplexerProvider DefaultConnectionMultiplexerProvider = new DefaultConnectionMultiplexerProvider(); + private static readonly IDatabaseProvider DefaultDatabaseProvider = new DefaultDatabaseProvider(); + private static readonly System.Type DefaultRegionStrategyType = typeof(DefaultRegionStrategy); + + private CacheSerializerBase _serializer; + private ICacheRegionStrategyFactory _regionStrategyFactory; + private IConnectionMultiplexerProvider _connectionMultiplexerProvider; + private IDatabaseProvider _databaseProvider; + private System.Type _defaultRegionStrategy; + + /// + /// The instance. + /// + public CacheSerializerBase Serializer + { + get => _serializer ?? DefaultSerializer; + set => _serializer = value; + } + + /// + /// The prefix that will be prepended before each cache key in order to avoid having collisions when multiple clients + /// uses the same Redis database. + /// + public string CacheKeyPrefix { get; set; } = "NHibernate-Cache:"; + + /// + /// The name of the environment that will be prepended before each cache key in order to allow having + /// multiple environments on the same Redis database. + /// + public string EnvironmentName { get; set; } + + /// + /// The prefix that will be prepended before the region name when building a cache key. + /// + public string RegionPrefix { get; set; } + + /// + /// Should the expiration delay be sliding? + /// + /// for resetting a cached item expiration each time it is accessed. + public bool DefaultUseSlidingExpiration { get; set; } + + /// + /// Whether the hash code of the key should be added to the cache key. + /// + public bool DefaultAppendHashcode { get; set; } + + /// + /// The default expiration time for the keys to expire. + /// + public TimeSpan DefaultExpiration { get; set; } = TimeSpan.FromSeconds(300); + + /// + /// The default Redis database index. + /// + public int DefaultDatabase { get; set; } = -1; + + /// + /// The instance. + /// + public ICacheRegionStrategyFactory RegionStrategyFactory + { + get => _regionStrategyFactory ?? DefaultRegionStrategyFactory; + set => _regionStrategyFactory = value; + } + + /// + /// The instance. + /// + public IConnectionMultiplexerProvider ConnectionMultiplexerProvider + { + get => _connectionMultiplexerProvider ?? DefaultConnectionMultiplexerProvider; + set => _connectionMultiplexerProvider = value; + } + + /// + /// The instance. + /// + public IDatabaseProvider DatabaseProvider + { + get => _databaseProvider ?? DefaultDatabaseProvider; + set => _databaseProvider = value; + } + + /// + /// The default type. + /// + public System.Type DefaultRegionStrategy + { + get => _defaultRegionStrategy ?? DefaultRegionStrategyType; + set => _defaultRegionStrategy = value; + } + + /// + /// The configuration for locking keys. + /// + public RedisCacheLockConfiguration LockConfiguration { get; } = new RedisCacheLockConfiguration(); + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheLockConfiguration.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheLockConfiguration.cs new file mode 100644 index 00000000..d270f12e --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheLockConfiguration.cs @@ -0,0 +1,80 @@ +using System; +using System.Text; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Cache configuration for locking keys. + /// + public class RedisCacheLockConfiguration + { + private static readonly ICacheLockValueProvider DefaultValueProvider = new DefaultCacheLockValueProvider(); + private static readonly ICacheLockRetryDelayProvider DefaultRetryDelayProvider = new DefaultCacheLockRetryDelayProvider(); + + private ICacheLockValueProvider _valueProvider; + private ICacheLockRetryDelayProvider _retryDelayProvider; + + /// + /// The timeout for a lock key to expire. + /// + public TimeSpan KeyTimeout { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// The suffix for the lock key. + /// + public string KeySuffix { get; set; } = ":lock"; + + /// + /// The time limit to acquire the lock. + /// + public TimeSpan AcquireTimeout { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// The number of retries for acquiring the lock. + /// + public int RetryTimes { get; set; } = 3; + + /// + /// The maximum delay before retrying to acquire the lock. + /// + public TimeSpan MaxRetryDelay { get; set; } = TimeSpan.FromMilliseconds(400); + + /// + /// The minimum delay before retrying to acquire the lock. + /// + public TimeSpan MinRetryDelay { get; set; } = TimeSpan.FromMilliseconds(10); + + /// + /// The instance. + /// + public ICacheLockValueProvider ValueProvider + { + get => _valueProvider ?? DefaultValueProvider; + set => _valueProvider = value; + } + + /// + /// The instance. + /// + public ICacheLockRetryDelayProvider RetryDelayProvider + { + get => _retryDelayProvider ?? DefaultRetryDelayProvider; + set => _retryDelayProvider = value; + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("KeyTimeout={0}s", KeyTimeout.TotalSeconds); + sb.AppendFormat("KeySuffix=({0})", KeySuffix); + sb.AppendFormat("AcquireTimeout={0}s", AcquireTimeout.TotalSeconds); + sb.AppendFormat("RetryTimes={0}", RetryTimes); + sb.AppendFormat("MaxRetryDelay={0}ms", MaxRetryDelay.TotalMilliseconds); + sb.AppendFormat("MinRetryDelay={0}ms", MinRetryDelay.TotalMilliseconds); + sb.AppendFormat("ValueProvider={0}", ValueProvider); + sb.AppendFormat("RetryDelayProvider={0}", RetryDelayProvider); + return sb.ToString(); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheProvider.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheProvider.cs new file mode 100644 index 00000000..391714a8 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheProvider.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using NHibernate.Cache; +using StackExchange.Redis; +using static NHibernate.Caches.StackExchangeRedis.ConfigurationHelper; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Cache provider using the classes. + /// + public class RedisCacheProvider : ICacheProvider + { + private static readonly INHibernateLogger Log; + private static readonly Dictionary ConfiguredCacheRegions; + private static readonly CacheConfig ConfiguredCache; + private static RedisCacheConfiguration _defaultCacheConfiguration = new RedisCacheConfiguration(); + + private IConnectionMultiplexer _connectionMultiplexer; + + /// + /// The default configuration that will be used for creating the . + /// + public static RedisCacheConfiguration DefaultCacheConfiguration + { + get => _defaultCacheConfiguration; + set => _defaultCacheConfiguration = value ?? new RedisCacheConfiguration(); + } + + static RedisCacheProvider() + { + Log = NHibernateLogger.For(typeof(RedisCacheProvider)); + ConfiguredCacheRegions = new Dictionary(); + + if (!(ConfigurationManager.GetSection("redis") is CacheConfig config)) + return; + + ConfiguredCache = config; + foreach (var cache in config.Regions) + { + ConfiguredCacheRegions.Add(cache.Region, cache); + } + } + + private static RedisCacheConfiguration CreateCacheConfiguration() + { + var defaultConfiguration = DefaultCacheConfiguration; + var defaultLockConfiguration = defaultConfiguration.LockConfiguration; + return new RedisCacheConfiguration + { + DefaultRegionStrategy = defaultConfiguration.DefaultRegionStrategy, + DatabaseProvider = defaultConfiguration.DatabaseProvider, + ConnectionMultiplexerProvider = defaultConfiguration.ConnectionMultiplexerProvider, + RegionPrefix = defaultConfiguration.RegionPrefix, + LockConfiguration = + { + RetryTimes = defaultLockConfiguration.RetryTimes, + RetryDelayProvider = defaultLockConfiguration.RetryDelayProvider, + MaxRetryDelay = defaultLockConfiguration.MaxRetryDelay, + ValueProvider = defaultLockConfiguration.ValueProvider, + KeyTimeout = defaultLockConfiguration.KeyTimeout, + AcquireTimeout = defaultLockConfiguration.AcquireTimeout, + KeySuffix = defaultLockConfiguration.KeySuffix, + MinRetryDelay = defaultLockConfiguration.MinRetryDelay + }, + Serializer = defaultConfiguration.Serializer, + RegionStrategyFactory = defaultConfiguration.RegionStrategyFactory, + CacheKeyPrefix = defaultConfiguration.CacheKeyPrefix, + DefaultUseSlidingExpiration = defaultConfiguration.DefaultUseSlidingExpiration, + DefaultExpiration = defaultConfiguration.DefaultExpiration, + DefaultDatabase = defaultConfiguration.DefaultDatabase, + DefaultAppendHashcode = defaultConfiguration.DefaultAppendHashcode, + EnvironmentName = defaultConfiguration.EnvironmentName + }; + } + + /// + /// The Redis cache configuration that is populated by the NHibernate configuration. + /// + public RedisCacheConfiguration CacheConfiguration { get; } = CreateCacheConfiguration(); + + /// +#pragma warning disable 618 + public ICache BuildCache(string regionName, IDictionary properties) +#pragma warning restore 618 + { + if (regionName == null) + { + regionName = string.Empty; + } + + var regionConfiguration = ConfiguredCacheRegions.TryGetValue(regionName, out var regionConfig) + ? BuildRegionConfiguration(regionConfig, properties) + : BuildRegionConfiguration(regionName, properties); + Log.Debug("Building cache: {0}", regionConfiguration.ToString()); + return BuildCache(regionConfiguration, properties); + } + + /// + public long NextTimestamp() + { + return Timestamper.Next(); + } + + /// + public void Start(IDictionary properties) + { + var configurationString = GetString(RedisEnvironment.Configuration, properties, ConfiguredCache?.Configuration); + if (string.IsNullOrEmpty(configurationString)) + { + throw new CacheException("The StackExchange.Redis configuration string was not provided."); + } + + Log.Debug("Starting with configuration string: {0}", configurationString); + BuildDefaultConfiguration(properties); + Start(configurationString, properties); + } + + /// + public virtual void Stop() + { + try + { + if (Log.IsDebugEnabled()) + { + Log.Debug("Releasing connection."); + } + + _connectionMultiplexer.Dispose(); + _connectionMultiplexer = null; + } + catch (Exception e) + { + Log.Error(e, "An error occurred while releasing the connection."); + } + } + + /// + /// Callback to perform any necessary initialization of the underlying cache implementation + /// during ISessionFactory construction. + /// + /// The StackExchange.Redis configuration string. + /// NHibernate configuration settings. + protected virtual void Start(string configurationString, IDictionary properties) + { + _connectionMultiplexer = CacheConfiguration.ConnectionMultiplexerProvider.Get(configurationString); + } + + /// + /// Builds the cache. + /// + /// The region cache configuration. + /// NHibernate configuration settings. + /// The built cache. + protected virtual CacheBase BuildCache(RedisCacheRegionConfiguration regionConfiguration, IDictionary properties) + { + var regionStrategy = + CacheConfiguration.RegionStrategyFactory.Create(_connectionMultiplexer, regionConfiguration, properties); + + regionStrategy.Validate(); + + return new RedisCache(regionConfiguration.RegionName, regionStrategy); + } + private RedisCacheRegionConfiguration BuildRegionConfiguration(string regionName, IDictionary properties) + { + return BuildRegionConfiguration(new RegionConfig(regionName), properties); + } + + private RedisCacheRegionConfiguration BuildRegionConfiguration(RegionConfig regionConfig, IDictionary properties) + { + var config = new RedisCacheRegionConfiguration(regionConfig.Region) + { + LockConfiguration = CacheConfiguration.LockConfiguration, + RegionPrefix = CacheConfiguration.RegionPrefix, + Serializer = CacheConfiguration.Serializer, + EnvironmentName = CacheConfiguration.EnvironmentName, + CacheKeyPrefix = CacheConfiguration.CacheKeyPrefix, + DatabaseProvider = CacheConfiguration.DatabaseProvider + }; + + config.Database = GetInteger("database", properties, + regionConfig.Database ?? GetInteger(RedisEnvironment.Database, properties, + CacheConfiguration.DefaultDatabase)); + Log.Debug("Database for region {0}: {1}", regionConfig.Region, config.Database); + + config.Expiration = GetTimeSpanFromSeconds("expiration", properties, + regionConfig.Expiration ?? GetTimeSpanFromSeconds(Cfg.Environment.CacheDefaultExpiration, properties, + CacheConfiguration.DefaultExpiration)); + Log.Debug("Expiration for region {0}: {1} seconds", regionConfig.Region, config.Expiration.TotalSeconds); + + config.RegionStrategy = GetSystemType("strategy", properties, + regionConfig.RegionStrategy ?? GetSystemType(RedisEnvironment.RegionStrategy, properties, + CacheConfiguration.DefaultRegionStrategy)); + Log.Debug("Region strategy for region {0}: {1}", regionConfig.Region, config.RegionStrategy); + + config.UseSlidingExpiration = GetBoolean("sliding", properties, + regionConfig.UseSlidingExpiration ?? GetBoolean(RedisEnvironment.UseSlidingExpiration, properties, + CacheConfiguration.DefaultUseSlidingExpiration)); + + config.AppendHashcode = GetBoolean("append-hashcode", properties, + regionConfig.AppendHashcode ?? GetBoolean(RedisEnvironment.AppendHashcode, properties, + CacheConfiguration.DefaultAppendHashcode)); + + Log.Debug("Use sliding expiration for region {0}: {1}", regionConfig.Region, config.UseSlidingExpiration); + + return config; + } + + private void BuildDefaultConfiguration(IDictionary properties) + { + var config = CacheConfiguration; + + config.CacheKeyPrefix = GetString(RedisEnvironment.KeyPrefix, properties, config.CacheKeyPrefix); + Log.Debug("Cache key prefix: {0}", config.CacheKeyPrefix); + + config.EnvironmentName = GetString(RedisEnvironment.EnvironmentName, properties, config.EnvironmentName); + Log.Debug("Cache environment name: {0}", config.EnvironmentName); + + config.RegionPrefix = GetString(Cfg.Environment.CacheRegionPrefix, properties, config.RegionPrefix); + Log.Debug("Region prefix: {0}", config.RegionPrefix); + + config.Serializer = GetInstance(RedisEnvironment.Serializer, properties, config.Serializer, Log); + Log.Debug("Serializer: {0}", config.Serializer); + + config.RegionStrategyFactory = GetInstance(RedisEnvironment.RegionStrategyFactory, properties, config.RegionStrategyFactory, Log); + Log.Debug("Region strategy factory: {0}", config.RegionStrategyFactory); + + config.ConnectionMultiplexerProvider = GetInstance(RedisEnvironment.ConnectionMultiplexerProvider, properties, config.ConnectionMultiplexerProvider, Log); + Log.Debug("Connection multiplexer provider: {0}", config.ConnectionMultiplexerProvider); + + config.DatabaseProvider = GetInstance(RedisEnvironment.DatabaseProvider, properties, config.DatabaseProvider, Log); + Log.Debug("Database provider: {0}", config.DatabaseProvider); + + config.DefaultExpiration = GetTimeSpanFromSeconds(Cfg.Environment.CacheDefaultExpiration, properties, config.DefaultExpiration); + Log.Debug("Default expiration: {0} seconds", config.DefaultExpiration.TotalSeconds); + + config.DefaultDatabase = GetInteger(RedisEnvironment.Database, properties, config.DefaultDatabase); + Log.Debug("Default database: {0}", config.DefaultDatabase); + + config.DefaultRegionStrategy = GetSystemType(RedisEnvironment.RegionStrategy, properties, config.DefaultRegionStrategy); + Log.Debug("Default region strategy: {0}", config.DefaultRegionStrategy); + + config.DefaultUseSlidingExpiration = GetBoolean(RedisEnvironment.UseSlidingExpiration, properties, + config.DefaultUseSlidingExpiration); + Log.Debug("Default use sliding expiration: {0}", config.DefaultUseSlidingExpiration); + + config.DefaultAppendHashcode = GetBoolean(RedisEnvironment.AppendHashcode, properties, + config.DefaultAppendHashcode); + Log.Debug("Default append hash code: {0}", config.DefaultAppendHashcode); + + var lockConfig = config.LockConfiguration; + lockConfig.KeyTimeout = GetTimeSpanFromSeconds(RedisEnvironment.LockKeyTimeout, properties, lockConfig.KeyTimeout); + Log.Debug("Lock key timeout: {0} seconds", lockConfig.KeyTimeout.TotalSeconds); + + lockConfig.AcquireTimeout = GetTimeSpanFromSeconds(RedisEnvironment.LockAcquireTimeout, properties, lockConfig.AcquireTimeout); + Log.Debug("Lock acquire timeout: {0} seconds", lockConfig.AcquireTimeout.TotalSeconds); + + lockConfig.RetryTimes = GetInteger(RedisEnvironment.LockRetryTimes, properties, lockConfig.RetryTimes); + Log.Debug("Lock retry times: {0}", lockConfig.RetryTimes); + + lockConfig.MaxRetryDelay = GetTimeSpanFromMilliseconds(RedisEnvironment.LockMaxRetryDelay, properties, lockConfig.MaxRetryDelay); + Log.Debug("Lock max retry delay: {0} milliseconds", lockConfig.MaxRetryDelay.TotalMilliseconds); + + lockConfig.MinRetryDelay = GetTimeSpanFromMilliseconds(RedisEnvironment.LockMinRetryDelay, properties, lockConfig.MinRetryDelay); + Log.Debug("Lock min retry delay: {0} milliseconds", lockConfig.MinRetryDelay.TotalMilliseconds); + + lockConfig.ValueProvider = GetInstance(RedisEnvironment.LockValueProvider, properties, lockConfig.ValueProvider, Log); + Log.Debug("Lock value provider: {0}", lockConfig.ValueProvider); + + lockConfig.RetryDelayProvider = GetInstance(RedisEnvironment.LockRetryDelayProvider, properties, lockConfig.RetryDelayProvider, Log); + Log.Debug("Lock retry delay provider: {0}", lockConfig.RetryDelayProvider); + + lockConfig.KeySuffix = GetString(RedisEnvironment.LockKeySuffix, properties, lockConfig.KeySuffix); + Log.Debug("Lock key suffix: {0}", lockConfig.KeySuffix); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheRegionConfiguration.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheRegionConfiguration.cs new file mode 100644 index 00000000..24b9637b --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisCacheRegionConfiguration.cs @@ -0,0 +1,107 @@ +using System; +using System.Text; +using NHibernate.Caches.Common; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Cache configuration for a region. + /// + public class RedisCacheRegionConfiguration + { + /// + /// Creates a cache region configuration. + /// + /// The name of the region. + public RedisCacheRegionConfiguration(string regionName) + { + RegionName = regionName; + } + + /// + /// The key representing the region that is composed of , + /// , and . + /// + public string RegionKey => $"{CacheKeyPrefix}{EnvironmentName}{RegionPrefix}{RegionName}"; + + /// + /// The region name. + /// + public string RegionName { get; } + + /// + /// The name of the environment that will be prepended before each cache key in order to allow having + /// multiple environments on the same Redis database. + /// + public string EnvironmentName { get; internal set; } + + /// + /// The prefix that will be prepended before each cache key in order to avoid having collisions when multiple clients + /// uses the same Redis database. + /// + public string CacheKeyPrefix { get; internal set; } + + /// + /// The expiration time for the keys to expire. + /// + public TimeSpan Expiration { get; internal set; } + + /// + /// The prefix that will be prepended before the region name when building a cache key. + /// + public string RegionPrefix { get; internal set; } + + /// + /// The Redis database index. + /// + public int Database { get; internal set; } + + /// + /// Whether the expiration is sliding or not. + /// + public bool UseSlidingExpiration { get; internal set; } + + /// + /// The type. + /// + public System.Type RegionStrategy { get; internal set; } + + /// + /// Whether the hash code of the key should be added to the cache key. + /// + public bool AppendHashcode { get; internal set; } + + /// + /// The to be used. + /// + public CacheSerializerBase Serializer { get; internal set; } + + /// + /// The instance. + /// + public IDatabaseProvider DatabaseProvider { get; internal set; } + + /// + /// The configuration for locking keys. + /// + public RedisCacheLockConfiguration LockConfiguration { get; internal set; } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendFormat("RegionName={0}", RegionName); + sb.AppendFormat("EnvironmentName={0}", EnvironmentName); + sb.AppendFormat("CacheKeyPrefix={0}", CacheKeyPrefix); + sb.AppendFormat("Expiration={0}s", Expiration.TotalSeconds); + sb.AppendFormat("Database={0}", Database); + sb.AppendFormat("UseSlidingExpiration={0}", UseSlidingExpiration); + sb.AppendFormat("AppendHashcode={0}", AppendHashcode); + sb.AppendFormat("RegionStrategy={0}", RegionStrategy); + sb.AppendFormat("Serializer={0}", Serializer); + sb.AppendFormat("DatabaseProvider={0}", DatabaseProvider); + sb.AppendFormat("LockConfiguration=({0})", LockConfiguration); + return sb.ToString(); + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisEnvironment.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisEnvironment.cs new file mode 100644 index 00000000..49b073cb --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisEnvironment.cs @@ -0,0 +1,107 @@ +using NHibernate.Cfg; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// An extension of NHibernate that provides configuration for the Redis cache. + /// + public static class RedisEnvironment + { + /// + /// The StackExchange.Redis configuration string. + /// + public const string Configuration = "cache.configuration"; + + /// + /// The Redis database index. + /// + public const string Database = "cache.database"; + + /// + /// The name of the environment that will be prepended before each cache key in order to allow having + /// multiple environments on the same Redis database. + /// + public const string EnvironmentName = "cache.environment_name"; + + /// + /// The assembly qualified name of the serializer. + /// + public const string Serializer = "cache.serializer"; + + /// + /// The assembly qualified name of the region strategy. + /// + public const string RegionStrategy = "cache.region_strategy"; + + /// + /// The assembly qualified name of the region strategy factory. + /// + public const string RegionStrategyFactory = "cache.region_strategy_factory"; + + /// + /// The assembly qualified name of the connection multiplexer provider. + /// + public const string ConnectionMultiplexerProvider = "cache.connection_multiplexer_provider"; + + /// + /// The assembly qualified name of the database provider. + /// + public const string DatabaseProvider = "cache.database_provider"; + + /// + /// Whether the expiration delay should be sliding. + /// + public const string UseSlidingExpiration = "cache.use_sliding_expiration"; + + /// + /// Whether the hash code of the key should be added to the cache key. + /// + public const string AppendHashcode = "cache.append_hashcode"; + + /// + /// The prefix that will be prepended before each cache key in order to avoid having collisions when multiple clients + /// uses the same Redis database. + /// + public const string KeyPrefix = "cache.key_prefix"; + + /// + /// The timeout for a lock key to expire in seconds. + /// + public const string LockKeyTimeout = "cache.lock.key_timeout"; + + /// + /// The time limit to acquire the lock in seconds. + /// + public const string LockAcquireTimeout = "cache.lock.acquire_timeout"; + + /// + /// The number of retries for acquiring the lock. + /// + public const string LockRetryTimes = "cache.lock.retry_times"; + + /// + /// The maximum delay before retrying to acquire the lock in milliseconds. + /// + public const string LockMaxRetryDelay = "cache.lock.max_retry_delay"; + + /// + /// The minimum delay before retrying to acquire the lock in milliseconds. + /// + public const string LockMinRetryDelay = "cache.lock.min_retry_delay"; + + /// + /// The assembly qualified name of the lock value provider. + /// + public const string LockValueProvider = "cache.lock.value_provider"; + + /// + /// The assembly qualified name of the lock retry delay provider. + /// + public const string LockRetryDelayProvider = "cache.lock.retry_delay_provider"; + + /// + /// The suffix for the lock key. + /// + public const string LockKeySuffix = "cache.lock.key_suffix"; + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisKeyLocker.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisKeyLocker.cs new file mode 100644 index 00000000..812ecd69 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisKeyLocker.cs @@ -0,0 +1,225 @@ +using System; +using System.Linq; +using NHibernate.Cache; +using StackExchange.Redis; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Provides a mechanism for locking and unlocking one or many keys. + /// + internal partial class RedisKeyLocker + { + private readonly string _lockKeySuffix; + private readonly TimeSpan _lockTimeout; + private readonly ICacheLockValueProvider _lockValueProvider; + private readonly IDatabase _database; + private readonly RetryPolicy> _retryPolicy; + + /// + /// Default constructor. + /// + /// The region name. + /// The Redis database. + /// The lock configuration. + public RedisKeyLocker( + string regionName, + IDatabase database, + RedisCacheLockConfiguration configuration) + { + _database = database; + _lockKeySuffix = configuration.KeySuffix; + _lockTimeout = configuration.KeyTimeout; + _lockValueProvider = configuration.ValueProvider; + + var acquireTimeout = configuration.AcquireTimeout; + var retryTimes = configuration.RetryTimes; + var maxRetryDelay = configuration.MaxRetryDelay; + var minRetryDelay = configuration.MinRetryDelay; + var lockRetryDelayProvider = configuration.RetryDelayProvider; + + _retryPolicy = new RetryPolicy>( + retryTimes, + acquireTimeout, + () => lockRetryDelayProvider.GetValue(minRetryDelay, maxRetryDelay) + ) + .ShouldRetry(s => s == null) + .OnFailure((totalAttempts, elapsedMs, getKeysFn) => + throw new CacheException("Unable to acquire cache lock: " + + $"region='{regionName}', " + + $"keys='{getKeysFn()}', " + + $"total attempts='{totalAttempts}', " + + $"total acquiring time= '{elapsedMs}ms'")); + } + + /// + /// Tries to lock the given key. + /// + /// The key to lock. + /// The lua script to lock the key. + /// The extra keys that will be provided to the + /// The extra values that will be provided to the + /// The lock value used to lock the key. + /// Thrown if the lock was not acquired. + public string Lock(string key, string luaScript, RedisKey[] extraKeys, RedisValue[] extraValues) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + var lockKey = $"{key}{_lockKeySuffix}"; + string Context() => lockKey; + + return _retryPolicy.Execute(() => + { + var lockValue = _lockValueProvider.GetValue(); + if (!string.IsNullOrEmpty(luaScript)) + { + var keys = new RedisKey[] {lockKey}; + if (extraKeys != null) + { + keys = keys.Concat(extraKeys).ToArray(); + } + var values = new RedisValue[] {lockValue, (long) _lockTimeout.TotalMilliseconds}; + if (extraValues != null) + { + values = values.Concat(extraValues).ToArray(); + } + var result = (RedisValue[]) _database.ScriptEvaluate(luaScript, keys, values); + if ((bool) result[0]) + { + return lockValue; + } + } + else if (_database.LockTake(lockKey, lockValue, _lockTimeout)) + { + return lockValue; + } + + return null; // retry + }, Context); + } + + /// + /// Tries to lock the given keys. + /// + /// The keys to lock. + /// The lua script to lock the keys. + /// The extra keys that will be provided to the + /// The extra values that will be provided to the + /// The lock value used to lock the keys. + /// Thrown if the lock was not acquired. + public string LockMany(string[] keys, string luaScript, RedisKey[] extraKeys, RedisValue[] extraValues) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + if (luaScript == null) + { + throw new ArgumentNullException(nameof(luaScript)); + } + string Context() => string.Join(",", keys.Select(o => $"{o}{_lockKeySuffix}")); + + return _retryPolicy.Execute(() => + { + var lockKeys = new RedisKey[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + lockKeys[i] = $"{keys[i]}{_lockKeySuffix}"; + } + var lockValue = _lockValueProvider.GetValue(); + if (extraKeys != null) + { + lockKeys = lockKeys.Concat(extraKeys).ToArray(); + } + var values = new RedisValue[] {lockValue, (long) _lockTimeout.TotalMilliseconds}; + if (extraValues != null) + { + values = values.Concat(extraValues).ToArray(); + } + var result = (RedisValue[]) _database.ScriptEvaluate(luaScript, lockKeys, values); + if ((bool) result[0]) + { + return lockValue; + } + + return null; // retry + }, Context); + } + + /// + /// Tries to unlock the given key. + /// + /// The key to unlock. + /// The value that was used to lock the key. + /// The lua script to unlock the key. + /// The extra keys that will be provided to the + /// The extra values that will be provided to the + /// Whether the key was unlocked. + public bool Unlock(string key, string lockValue, string luaScript, RedisKey[] extraKeys, RedisValue[] extraValues) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + var lockKey = $"{key}{_lockKeySuffix}"; + if (string.IsNullOrEmpty(luaScript)) + { + return _database.LockRelease(lockKey, lockValue); + } + var keys = new RedisKey[] {lockKey}; + if (extraKeys != null) + { + keys = keys.Concat(extraKeys).ToArray(); + } + var values = new RedisValue[] {lockValue}; + if (extraValues != null) + { + values = values.Concat(extraValues).ToArray(); + } + + var result = (RedisValue[]) _database.ScriptEvaluate(luaScript, keys, values); + return (bool) result[0]; + } + + /// + /// Tries to unlock the given keys. + /// + /// The keys to unlock. + /// The value that was used to lock the keys. + /// The lua script to unlock the keys. + /// The extra keys that will be provided to the + /// The extra values that will be provided to the + /// How many keys were unlocked. + public int UnlockMany(string[] keys, string lockValue, string luaScript, RedisKey[] extraKeys, RedisValue[] extraValues) + { + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + if (luaScript == null) + { + throw new ArgumentNullException(nameof(luaScript)); + } + + var lockKeys = new RedisKey[keys.Length]; + for (var i = 0; i < keys.Length; i++) + { + lockKeys[i] = $"{keys[i]}{_lockKeySuffix}"; + } + if (extraKeys != null) + { + lockKeys = lockKeys.Concat(extraKeys).ToArray(); + } + var values = new RedisValue[] {lockValue}; + if (extraValues != null) + { + values = values.Concat(extraValues).ToArray(); + } + + var result = (RedisValue[]) _database.ScriptEvaluate(luaScript, lockKeys, values); + return (int) result[0]; + } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisSectionHandler.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisSectionHandler.cs new file mode 100644 index 00000000..260cec96 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RedisSectionHandler.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Xml; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Configuration file provider. + /// + public class RedisSectionHandler : IConfigurationSectionHandler + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(RedisSectionHandler)); + + #region IConfigurationSectionHandler Members + + /// + /// A object. + public object Create(object parent, object configContext, XmlNode section) + { + var configuration = section.Attributes?["configuration"]?.Value; + var regions = new List(); + + var nodes = section.SelectNodes("cache"); + foreach (XmlNode node in nodes) + { + var region = node.Attributes?["region"]?.Value; + if (region != null) + { + regions.Add(new RegionConfig( + region, + GetTimespanFromSeconds(node, "expiration"), + GetBoolean(node, "sliding"), + GetInteger(node, "database"), + GetType(node, "strategy"), + GetBoolean(node, "append-hashcode") + )); + } + else + { + Log.Warn("Found a cache region node lacking a region name: ignored. Node: {0}", + node.OuterXml); + } + } + return new CacheConfig(configuration, regions.ToArray()); + } + + private static TimeSpan? GetTimespanFromSeconds(XmlNode node, string attributeName) + { + var seconds = GetInteger(node, attributeName); + if (!seconds.HasValue) + { + return null; + } + return TimeSpan.FromSeconds(seconds.Value); + } + + private static bool? GetBoolean(XmlNode node, string attributeName) + { + var value = node.Attributes?[attributeName]?.Value; + if (string.IsNullOrEmpty(value)) + { + return null; + } + if (bool.TryParse(value, out var boolean)) + { + return boolean; + } + + Log.Warn("Invalid value for boolean attribute {0}: ignored. Node: {1}", attributeName, node.OuterXml); + return null; + } + + private static System.Type GetType(XmlNode node, string attributeName) + { + var value = node.Attributes?[attributeName]?.Value; + if (string.IsNullOrEmpty(value)) + { + return null; + } + try + { + return System.Type.GetType(value, true); + } + catch (Exception e) + { + Log.Warn("Unable to acquire type for attribute {0}: ignored. Node: {1}, Exception: {2}", attributeName, node.OuterXml, e); + return null; + } + } + + private static int? GetInteger(XmlNode node, string attributeName) + { + var value = node.Attributes?[attributeName]?.Value; + if (string.IsNullOrEmpty(value)) + { + return null; + } + if (int.TryParse(value, out var number)) + { + return number; + } + + Log.Warn("Invalid value for integer attribute {0}: ignored. Node: {1}", attributeName, node.OuterXml); + return null; + } + + #endregion + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RegionConfig.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RegionConfig.cs new file mode 100644 index 00000000..6f57f6cb --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RegionConfig.cs @@ -0,0 +1,68 @@ +using System; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// Region configuration properties. + /// + public class RegionConfig + { + /// + /// Build a cache region configuration. + /// + /// The configured cache region. + public RegionConfig(string region) : this(region, null, null, null, null, null) + { + } + + /// + /// Build a cache region configuration. + /// + /// The configured cache region. + /// The expiration for the region. + /// Whether the expiration should be sliding or not. + /// The database for the region. + /// The strategy for the region. + /// Whether the hash code of the key should be added to the cache key. + public RegionConfig(string region, TimeSpan? expiration, bool? useSlidingExpiration, int? database, System.Type regionStrategy, + bool? appendHashcode) + { + Region = region; + Expiration = expiration; + UseSlidingExpiration = useSlidingExpiration; + Database = database; + RegionStrategy = regionStrategy; + AppendHashcode = appendHashcode; + } + + /// + /// The region name. + /// + public string Region { get; } + + /// + /// The expiration time for the keys to expire. + /// + public TimeSpan? Expiration { get; } + + /// + /// Whether the expiration is sliding or not. + /// + public bool? UseSlidingExpiration { get; } + + /// + /// The Redis database index. + /// + public int? Database { get; } + + /// + /// The type. + /// + public System.Type RegionStrategy { get; } + + /// + /// Whether the hash code of the key should be added to the cache key. + /// + public bool? AppendHashcode { get; } + } +} diff --git a/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RetryPolicy.cs b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RetryPolicy.cs new file mode 100644 index 00000000..adfe0513 --- /dev/null +++ b/StackExchangeRedis/NHibernate.Caches.StackExchangeRedis/RetryPolicy.cs @@ -0,0 +1,93 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace NHibernate.Caches.StackExchangeRedis +{ + /// + /// A retry policy that can be applied to delegates returning a value of type . + /// + /// The type of the execution result. + /// The context¸that can be used in callbacks. + internal class RetryPolicy + { + private readonly int _retryTimes; + private readonly Func _retryDelayFunc; + private readonly double _maxAllowedTime; + private Predicate _shouldRetryPredicate; + private Action _onFailureCallback; + + public RetryPolicy(int retryTimes, TimeSpan maxAllowedTime, Func retryDelayFunc) + { + _retryTimes = retryTimes; + _retryDelayFunc = retryDelayFunc; + _maxAllowedTime = maxAllowedTime.TotalMilliseconds; + } + + public RetryPolicy ShouldRetry(Predicate predicate) + { + _shouldRetryPredicate = predicate; + return this; + } + + public RetryPolicy OnFailure(Action callback) + { + _onFailureCallback = callback; + return this; + } + + public TResult Execute(Func func, TContext context) + { + var totalAttempts = 0; + var timer = new Stopwatch(); + timer.Start(); + do + { + if (totalAttempts > 0) + { + var retryDelay = _retryDelayFunc(); + Thread.Sleep(retryDelay); + } + + var result = func(); + if (_shouldRetryPredicate?.Invoke(result) != true) + { + return result; + } + totalAttempts++; + + } while (_retryTimes > totalAttempts - 1 && timer.ElapsedMilliseconds < _maxAllowedTime); + timer.Stop(); + _onFailureCallback?.Invoke(totalAttempts, timer.ElapsedMilliseconds, context); + return default(TResult); + } + + public async Task ExecuteAsync(Func> func, TContext context, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var totalAttempts = 0; + var timer = new Stopwatch(); + timer.Start(); + do + { + if (totalAttempts > 0) + { + var retryDelay = _retryDelayFunc(); + await Task.Delay(retryDelay, cancellationToken).ConfigureAwait(false); + } + + var result = await func().ConfigureAwait(false); + if (_shouldRetryPredicate?.Invoke(result) != true) + { + return result; + } + totalAttempts++; + + } while (_retryTimes > totalAttempts - 1 && timer.ElapsedMilliseconds < _maxAllowedTime); + timer.Stop(); + _onFailureCallback?.Invoke(totalAttempts, timer.ElapsedMilliseconds, context); + return default(TResult); + } + } +} diff --git a/StackExchangeRedis/default.build b/StackExchangeRedis/default.build new file mode 100644 index 00000000..0f8c6a20 --- /dev/null +++ b/StackExchangeRedis/default.build @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tools/packages.config b/Tools/packages.config index d8019d39..cb540977 100644 --- a/Tools/packages.config +++ b/Tools/packages.config @@ -7,5 +7,5 @@ - + diff --git a/appveyor.yml b/appveyor.yml index 527878e4..e0a8afaa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,6 +19,10 @@ install: - curl -L -O -S -s http://downloads.northscale.com/memcached-1.4.5-amd64.zip - 7z x memcached-1.4.5-amd64.zip - ps: $MemCached = Start-Process memcached-amd64\memcached.exe -PassThru +# redis server +- curl -L -O -S -s https://github.com/ServiceStack/redis-windows/raw/master/downloads/redis64-2.8.17.zip +- 7z x redis64-2.8.17.zip +- ps: $Redis = Start-Process redis-server.exe redis.windows.conf -PassThru before_build: - which msbuild.exe - nuget restore NHibernate.Caches.Everything.sln @@ -107,7 +111,7 @@ test_script: #netFx tests If ($env:TESTS -eq 'net') { - @('EnyimMemcached', 'Prevalence', 'RtMemoryCache', 'SysCache', 'SysCache2', 'CoreMemoryCache', 'CoreDistributedCache') | ForEach-Object { + @('EnyimMemcached', 'Prevalence', 'RtMemoryCache', 'SysCache', 'SysCache2', 'CoreMemoryCache', 'CoreDistributedCache', 'StackExchangeRedis') | ForEach-Object { $projects.Add($_, "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$target\NHibernate.Caches.$_.Tests.dll") } ForEach ($project in $projects.GetEnumerator()) { @@ -120,7 +124,7 @@ test_script: #core tests If ($env:TESTS -eq 'core') { - @('CoreMemoryCache', 'CoreDistributedCache', 'RtMemoryCache') | ForEach-Object { + @('CoreMemoryCache', 'CoreDistributedCache', 'RtMemoryCache', 'StackExchangeRedis') | ForEach-Object { $projects.Add($_, "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$target\NHibernate.Caches.$_.Tests.dll") } ForEach ($project in $projects.GetEnumerator()) { @@ -151,3 +155,4 @@ artifacts: name: Binaries on_finish: - ps: Stop-Process -Id $MemCached.Id +- ps: Stop-Process -Id $Redis.Id diff --git a/default.build b/default.build index 637138d7..e1f1cf0c 100644 --- a/default.build +++ b/default.build @@ -19,6 +19,7 @@ +