Skip to content

Commit baf0c87

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

File tree

12 files changed

+148
-22
lines changed

12 files changed

+148
-22
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
{
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
#region License
12+
13+
//
14+
// CoreDistributedCache - A cache provider for NHibernate using Microsoft.Extensions.Caching.Distributed.IDistributedCache.
15+
//
16+
// This library is free software; you can redistribute it and/or
17+
// modify it under the terms of the GNU Lesser General Public
18+
// License as published by the Free Software Foundation; either
19+
// version 2.1 of the License, or (at your option) any later version.
20+
//
21+
// This library is distributed in the hope that it will be useful,
22+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
23+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24+
// Lesser General Public License for more details.
25+
//
26+
// You should have received a copy of the GNU Lesser General Public
27+
// License along with this library; if not, write to the Free Software
28+
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
29+
//
30+
31+
#endregion
32+
33+
using System;
34+
using System.Collections.Generic;
35+
using Microsoft.Extensions.Caching.Distributed;
36+
using NHibernate.Cache;
37+
using NHibernate.Caches.Common.Tests;
38+
using NUnit.Framework;
39+
using NSubstitute;
40+
41+
namespace NHibernate.Caches.CoreDistributedCache.Tests
42+
{
43+
using System.Threading.Tasks;
44+
using System.Threading;
45+
public partial class CoreDistributedCacheFixture : CacheFixture
46+
{
47+
48+
[Test]
49+
public async Task MaxKeySizeAsync()
50+
{
51+
var distribCache = Substitute.For<IDistributedCache>();
52+
const int maxLength = 20;
53+
var cache = new CoreDistributedCache(distribCache, maxLength, "foo", new Dictionary<string, string>());
54+
await (cache.PutAsync(new string('k', maxLength * 2), "test", CancellationToken.None));
55+
await (distribCache.Received().SetAsync(Arg.Is<string>(k => k.Length <= maxLength), Arg.Any<byte[]>(),
56+
Arg.Any<DistributedCacheEntryOptions>()));
57+
}
58+
}
59+
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,33 @@
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
{
3033
[TestFixture]
31-
public class CoreDistributedCacheFixture : CacheFixture
34+
public partial class CoreDistributedCacheFixture : CacheFixture
3235
{
3336
protected override bool SupportsSlidingExpiration => true;
3437
protected override bool SupportsClear => false;
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/Async/CoreDistributedCache.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
using System.Collections.Generic;
1414
using System.IO;
1515
using System.Runtime.Serialization.Formatters.Binary;
16+
using System.Security.Cryptography;
17+
using System.Text;
1618
using Microsoft.Extensions.Caching.Distributed;
1719
using NHibernate.Util;
1820

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCache.cs

Lines changed: 39 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,13 @@ 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
{
79+
if (maxKeySize.HasValue && maxKeySize <= 0)
80+
throw new ArgumentException($"{nameof(maxKeySize)} must be null or superior to 1.", nameof(maxKeySize));
7381
_cache = cache;
82+
_maxKeySize = maxKeySize;
7483
RegionName = region;
7584
Configure(properties);
7685
}
@@ -161,7 +170,35 @@ private static bool GetUseSlidingExpiration(IDictionary<string, string> props)
161170

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

167204
/// <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 />

0 commit comments

Comments
 (0)