Skip to content

Commit 84acd34

Browse files
Support cache implementations having a max key length
1 parent e44b468 commit 84acd34

File tree

10 files changed

+84
-21
lines changed

10 files changed

+84
-21
lines changed

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/MemoryFactory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ public IDistributedCache BuildCache()
111111
return _cache;
112112
}
113113

114+
/// <inheritdoc />
115+
public int? MaxKeySize => null;
116+
114117
private class Options : MemoryDistributedCacheOptions, IOptions<MemoryDistributedCacheOptions>
115118
{
116119
MemoryDistributedCacheOptions IOptions<MemoryDistributedCacheOptions>.Value => this;

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/RedisFactory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ public RedisFactory(IDictionary<string, string> properties)
6262
Log.Info("No {0} property provided", _instanceName);
6363
}
6464

65+
/// <inheritdoc />
66+
public int? MaxKeySize => null;
67+
6568
/// <inheritdoc />
6669
public IDistributedCache BuildCache()
6770
{

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/SqlServerFactory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ public SqlServerFactory(IDictionary<string, string> properties)
9898
}
9999
}
100100

101+
/// <inheritdoc />
102+
public int? MaxKeySize => 449;
103+
101104
/// <inheritdoc />
102105
public IDistributedCache BuildCache()
103106
{

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheFixture.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
#endregion
2222

2323
using System;
24+
using System.Collections.Generic;
25+
using Microsoft.Extensions.Caching.Distributed;
2426
using NHibernate.Cache;
2527
using NHibernate.Caches.Common.Tests;
2628
using NUnit.Framework;
29+
using NSubstitute;
2730

2831
namespace NHibernate.Caches.CoreDistributedCache.Tests
2932
{
@@ -35,5 +38,16 @@ public class CoreDistributedCacheFixture : CacheFixture
3538

3639
protected override Func<ICacheProvider> ProviderBuilder =>
3740
() => new CoreDistributedCacheProvider();
41+
42+
[Test]
43+
public void MaxKeySize()
44+
{
45+
var distribCache = Substitute.For<IDistributedCache>();
46+
const int maxLength = 20;
47+
var cache = new CoreDistributedCache(distribCache, maxLength, "foo", new Dictionary<string, string>());
48+
cache.Put(new string('k', maxLength * 2), "test");
49+
distribCache.Received().Set(Arg.Is<string>(k => k.Length <= maxLength), Arg.Any<byte[]>(),
50+
Arg.Any<DistributedCacheEntryOptions>());
51+
}
3852
}
3953
}

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/NHibernate.Caches.CoreDistributedCache.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<ItemGroup>
2424
<PackageReference Include="log4net" Version="2.0.8" />
2525
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.1" />
26+
<PackageReference Include="NSubstitute" Version="3.1.0" />
2627
<PackageReference Include="NUnit3TestAdapter" Version="3.9.0" />
2728
</ItemGroup>
2829
<ItemGroup Condition="'$(TargetFramework)'=='netcoreapp2.0'">

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/TestsContext.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
#if !NETFX
2-
using log4net.Repository.Hierarchy;
1+
using log4net.Repository.Hierarchy;
2+
#if !NETFX
33
using NHibernate.Caches.Common.Tests;
4+
#endif
45
using NUnit.Framework;
56

67
namespace NHibernate.Caches.CoreDistributedCache.Tests
@@ -11,20 +12,19 @@ public class TestsContext
1112
[OneTimeSetUp]
1213
public void RunBeforeAnyTests()
1314
{
15+
#if !NETFX
1416
TestsContextHelper.RunBeforeAnyTests(typeof(TestsContext).Assembly, "coredistributedcache");
15-
//When .NET Core App 2.0 tests run from VS/VSTest the entry assembly is "testhost.dll"
16-
//so we need to explicitly load the configuration
17-
if (TestsContextHelper.ExecutingWithVsTest)
18-
{
19-
ConfigureLog4Net();
20-
}
17+
#endif
18+
ConfigureLog4Net();
2119
}
2220

21+
#if !NETFX
2322
[OneTimeTearDown]
2423
public void RunAfterAnyTests()
2524
{
2625
TestsContextHelper.RunAfterAnyTests();
2726
}
27+
#endif
2828

2929
private static void ConfigureLog4Net()
3030
{
@@ -40,4 +40,3 @@ private static void ConfigureLog4Net()
4040
}
4141
}
4242
}
43-
#endif

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCache.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
using System.Collections.Generic;
2626
using System.IO;
2727
using System.Runtime.Serialization.Formatters.Binary;
28+
using System.Security.Cryptography;
29+
using System.Text;
2830
using Microsoft.Extensions.Caching.Distributed;
2931
using NHibernate.Util;
3032

@@ -44,18 +46,21 @@ public partial class CoreDistributedCache : ICache
4446
private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(CoreDistributedCache));
4547

4648
private readonly IDistributedCache _cache;
49+
private readonly int? _maxKeySize;
4750

4851
private static readonly TimeSpan DefaultExpiration = TimeSpan.FromSeconds(300);
4952
private const bool _defaultUseSlidingExpiration = false;
5053
private static readonly string DefaultRegionPrefix = string.Empty;
5154
private const string _cacheKeyPrefix = "NHibernate-Cache:";
5255

5356
private string _fullRegion;
57+
private bool _hasWarnedOnHashLength;
5458

5559
/// <summary>
5660
/// Default constructor.
5761
/// </summary>
5862
/// <param name="cache">The <see cref="IDistributedCache"/> instance to use.</param>
63+
/// <param name="maxKeySize">If key size is limited, the maximal key size.</param>
5964
/// <param name="region">The region of the cache.</param>
6065
/// <param name="properties">Cache configuration properties.</param>
6166
/// <remarks>
@@ -68,9 +73,11 @@ public partial class CoreDistributedCache : ICache
6873
/// All parameters are optional. The defaults are an expiration of 300 seconds, no sliding expiration and no prefix.
6974
/// </remarks>
7075
/// <exception cref="ArgumentException">The "expiration" property could not be parsed.</exception>
71-
public CoreDistributedCache(IDistributedCache cache, string region, IDictionary<string, string> properties)
76+
public CoreDistributedCache(
77+
IDistributedCache cache, int? maxKeySize, string region, IDictionary<string, string> properties)
7278
{
7379
_cache = cache;
80+
_maxKeySize = maxKeySize;
7481
RegionName = region;
7582
Configure(properties);
7683
}
@@ -161,7 +168,35 @@ private static bool GetUseSlidingExpiration(IDictionary<string, string> props)
161168

162169
private string GetCacheKey(object key)
163170
{
164-
return string.Concat(_cacheKeyPrefix, _fullRegion, ":", key.ToString(), "@", key.GetHashCode());
171+
var keyAsString = string.Concat(_cacheKeyPrefix, _fullRegion, ":", key.ToString(), "@", key.GetHashCode());
172+
if (!_maxKeySize.HasValue || _maxKeySize >= keyAsString.Length)
173+
return keyAsString;
174+
175+
Log.Info(
176+
"Computing a hashed key for too long key '{0}'. This may cause collisions resulting into additional cache misses.",
177+
key);
178+
// Hash it for respecting max key size. Collisions will be avoided by storing the actual key along
179+
// the object and comparing it on retrieval.
180+
using (var hasher = new SHA256Managed())
181+
{
182+
var bytes = Encoding.UTF8.GetBytes(keyAsString);
183+
var computedHash = Convert.ToBase64String(hasher.ComputeHash(bytes));
184+
if (computedHash.Length <= _maxKeySize)
185+
return computedHash;
186+
187+
if (!_hasWarnedOnHashLength)
188+
{
189+
// No lock for this field, some redundant logs will be less harm than locking.
190+
_hasWarnedOnHashLength = true;
191+
Log.Warn(
192+
"Hash computed for too long keys are themselves too long. They will be truncated, further " +
193+
"increasing the risk of collision resulting into additional cache misses. Consider using a " +
194+
"cache supporting longer keys. Hash length: {0}; max key size: {1}",
195+
computedHash.Length, _maxKeySize);
196+
}
197+
198+
return computedHash.Substring(0, _maxKeySize.Value);
199+
}
165200
}
166201

167202
/// <inheritdoc />

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCacheProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public ICache BuildCache(string regionName, IDictionary<string, string> properti
138138

139139
Log.Debug("building cache with region: {0}, properties: {1}, factory: {2}" , regionName, sb.ToString(), CacheFactory.GetType().FullName);
140140
}
141-
return new CoreDistributedCache(CacheFactory.BuildCache(), regionName, properties);
141+
return new CoreDistributedCache(CacheFactory.BuildCache(), CacheFactory.MaxKeySize, regionName, properties);
142142
}
143143

144144
/// <inheritdoc />

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/IDistributedCacheFactory.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,11 @@ public interface IDistributedCacheFactory
1212
/// </summary>
1313
/// <returns>A <see cref="IDistributedCache"/> instance.</returns>
1414
IDistributedCache BuildCache();
15+
16+
/// <summary>
17+
/// If the underlying <see cref="IDistributedCache"/> implementation has a limit on key size,
18+
/// its maximal size. <see langword="null" /> otherwise.
19+
/// </summary>
20+
int? MaxKeySize { get; }
1521
}
1622
}

CoreMemoryCache/NHibernate.Caches.CoreMemoryCache.Tests/TestsContext.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
#if !NETFX
2-
using log4net.Repository.Hierarchy;
1+
using log4net.Repository.Hierarchy;
2+
#if !NETFX
33
using NHibernate.Caches.Common.Tests;
4+
#endif
45
using NUnit.Framework;
56

67
namespace NHibernate.Caches.CoreMemoryCache.Tests
@@ -11,20 +12,19 @@ public class TestsContext
1112
[OneTimeSetUp]
1213
public void RunBeforeAnyTests()
1314
{
15+
#if !NETFX
1416
TestsContextHelper.RunBeforeAnyTests(typeof(TestsContext).Assembly, "corememorycache");
15-
//When .NET Core App 2.0 tests run from VS/VSTest the entry assembly is "testhost.dll"
16-
//so we need to explicitly load the configuration
17-
if (TestsContextHelper.ExecutingWithVsTest)
18-
{
19-
ConfigureLog4Net();
20-
}
17+
#endif
18+
ConfigureLog4Net();
2119
}
2220

21+
#if !NETFX
2322
[OneTimeTearDown]
2423
public void RunAfterAnyTests()
2524
{
2625
TestsContextHelper.RunAfterAnyTests();
2726
}
27+
#endif
2828

2929
private static void ConfigureLog4Net()
3030
{
@@ -40,4 +40,3 @@ private static void ConfigureLog4Net()
4040
}
4141
}
4242
}
43-
#endif

0 commit comments

Comments
 (0)