Skip to content

Commit 771b6b8

Browse files
Fix GetQueryCache storing two different caches.
1 parent bf7e34d commit 771b6b8

File tree

3 files changed

+299
-3
lines changed

3 files changed

+299
-3
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
using System;
12+
using System.Collections;
13+
using System.Collections.Concurrent;
14+
using System.Collections.Generic;
15+
using System.Threading;
16+
using NHibernate.Cache;
17+
using NHibernate.Cfg;
18+
using NUnit.Framework;
19+
using Environment = NHibernate.Cfg.Environment;
20+
21+
namespace NHibernate.Test.CacheTest
22+
{
23+
using System.Threading.Tasks;
24+
[TestFixture]
25+
public class GetQueryCacheFixtureAsync : TestCase
26+
{
27+
protected override IList Mappings => new[] { "Simple.hbm.xml" };
28+
29+
protected override void Configure(Configuration configuration)
30+
{
31+
configuration.SetProperty(Environment.UseQueryCache, "true");
32+
configuration.SetProperty(Environment.CacheProvider, typeof(LockedCacheProvider).AssemblyQualifiedName);
33+
}
34+
35+
[Test]
36+
public async Task RetrievedQueryCacheMatchesGloballyStoredOneAsync()
37+
{
38+
var region = "RetrievedQueryCacheMatchesGloballyStoredOne";
39+
LockedCache.Semaphore = new SemaphoreSlim(0);
40+
try
41+
{
42+
var failures = new ConcurrentBag<Exception>();
43+
var thread1 = new Thread(
44+
() =>
45+
{
46+
try
47+
{
48+
Sfi.GetQueryCache(region);
49+
}
50+
catch (Exception e)
51+
{
52+
failures.Add(e);
53+
}
54+
});
55+
var thread2 = new Thread(
56+
() =>
57+
{
58+
try
59+
{
60+
Sfi.GetQueryCache(region);
61+
}
62+
catch (Exception e)
63+
{
64+
failures.Add(e);
65+
}
66+
});
67+
thread1.Start();
68+
thread2.Start();
69+
70+
// Give some time to threads for reaching the wait, having all of them ready to do most of their job concurrently.
71+
await (Task.Delay(100));
72+
// Let only one finish its job, it should be the one being stored in query cache dictionary.
73+
LockedCache.Semaphore.Release(1);
74+
// Give some time to released thread to finish its job.
75+
await (Task.Delay(100));
76+
// Release other thread
77+
LockedCache.Semaphore.Release(10);
78+
79+
thread1.Join();
80+
thread2.Join();
81+
Assert.That(failures, Is.Empty, $"{failures.Count} thread(s) failed.");
82+
}
83+
finally
84+
{
85+
LockedCache.Semaphore.Dispose();
86+
LockedCache.Semaphore = null;
87+
}
88+
89+
var queryCache = Sfi.GetQueryCache(region).Cache;
90+
var globalCache = Sfi.GetSecondLevelCacheRegion(region);
91+
Assert.That(globalCache, Is.SameAs(queryCache));
92+
}
93+
}
94+
95+
public partial class LockedCache : ICache
96+
{
97+
98+
#region Void implementation
99+
100+
public Task<object> GetAsync(object key, CancellationToken cancellationToken)
101+
{
102+
return Task.FromResult<object>(null);
103+
}
104+
105+
public Task PutAsync(object key, object value, CancellationToken cancellationToken)
106+
{
107+
return Task.CompletedTask;
108+
}
109+
110+
public Task RemoveAsync(object key, CancellationToken cancellationToken)
111+
{
112+
return Task.CompletedTask;
113+
}
114+
115+
public Task ClearAsync(CancellationToken cancellationToken)
116+
{
117+
return Task.CompletedTask;
118+
}
119+
120+
public Task LockAsync(object key, CancellationToken cancellationToken)
121+
{
122+
return Task.CompletedTask;
123+
}
124+
125+
public Task UnlockAsync(object key, CancellationToken cancellationToken)
126+
{
127+
return Task.CompletedTask;
128+
}
129+
130+
#endregion
131+
}
132+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Concurrent;
4+
using System.Collections.Generic;
5+
using System.Threading;
6+
using NHibernate.Cache;
7+
using NHibernate.Cfg;
8+
using NUnit.Framework;
9+
using Environment = NHibernate.Cfg.Environment;
10+
11+
namespace NHibernate.Test.CacheTest
12+
{
13+
[TestFixture]
14+
public class GetQueryCacheFixture : TestCase
15+
{
16+
protected override IList Mappings => new[] { "Simple.hbm.xml" };
17+
18+
protected override void Configure(Configuration configuration)
19+
{
20+
configuration.SetProperty(Environment.UseQueryCache, "true");
21+
configuration.SetProperty(Environment.CacheProvider, typeof(LockedCacheProvider).AssemblyQualifiedName);
22+
}
23+
24+
[Test]
25+
public void RetrievedQueryCacheMatchesGloballyStoredOne()
26+
{
27+
var region = "RetrievedQueryCacheMatchesGloballyStoredOne";
28+
LockedCache.Semaphore = new SemaphoreSlim(0);
29+
try
30+
{
31+
var failures = new ConcurrentBag<Exception>();
32+
var thread1 = new Thread(
33+
() =>
34+
{
35+
try
36+
{
37+
Sfi.GetQueryCache(region);
38+
}
39+
catch (Exception e)
40+
{
41+
failures.Add(e);
42+
}
43+
});
44+
var thread2 = new Thread(
45+
() =>
46+
{
47+
try
48+
{
49+
Sfi.GetQueryCache(region);
50+
}
51+
catch (Exception e)
52+
{
53+
failures.Add(e);
54+
}
55+
});
56+
thread1.Start();
57+
thread2.Start();
58+
59+
// Give some time to threads for reaching the wait, having all of them ready to do most of their job concurrently.
60+
Thread.Sleep(100);
61+
// Let only one finish its job, it should be the one being stored in query cache dictionary.
62+
LockedCache.Semaphore.Release(1);
63+
// Give some time to released thread to finish its job.
64+
Thread.Sleep(100);
65+
// Release other thread
66+
LockedCache.Semaphore.Release(10);
67+
68+
thread1.Join();
69+
thread2.Join();
70+
Assert.That(failures, Is.Empty, $"{failures.Count} thread(s) failed.");
71+
}
72+
finally
73+
{
74+
LockedCache.Semaphore.Dispose();
75+
LockedCache.Semaphore = null;
76+
}
77+
78+
var queryCache = Sfi.GetQueryCache(region).Cache;
79+
var globalCache = Sfi.GetSecondLevelCacheRegion(region);
80+
Assert.That(globalCache, Is.SameAs(queryCache));
81+
}
82+
}
83+
84+
public partial class LockedCache : ICache
85+
{
86+
public static SemaphoreSlim Semaphore { get; set; }
87+
88+
public LockedCache(string regionName)
89+
{
90+
RegionName = regionName;
91+
Semaphore?.Wait();
92+
}
93+
94+
#region Void implementation
95+
96+
public object Get(object key)
97+
{
98+
return null;
99+
}
100+
101+
public void Put(object key, object value)
102+
{
103+
}
104+
105+
public void Remove(object key)
106+
{
107+
}
108+
109+
public void Clear()
110+
{
111+
}
112+
113+
public void Destroy()
114+
{
115+
}
116+
117+
public void Lock(object key)
118+
{
119+
}
120+
121+
public void Unlock(object key)
122+
{
123+
}
124+
125+
public long NextTimestamp()
126+
{
127+
return Timestamper.Next();
128+
}
129+
130+
public int Timeout => Timestamper.OneMs * 60000;
131+
132+
public string RegionName { get; }
133+
134+
#endregion
135+
}
136+
137+
public class LockedCacheProvider : ICacheProvider
138+
{
139+
public ICache BuildCache(string regionName, IDictionary<string, string> properties)
140+
{
141+
return new LockedCache(regionName);
142+
}
143+
144+
public long NextTimestamp()
145+
{
146+
return Timestamper.Next();
147+
}
148+
149+
public void Start(IDictionary<string, string> properties)
150+
{
151+
}
152+
153+
public void Stop()
154+
{
155+
}
156+
}
157+
}

src/NHibernate/Impl/SessionFactoryImpl.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,14 +1027,21 @@ public IQueryCache GetQueryCache(string cacheRegion)
10271027
return null;
10281028
}
10291029

1030-
return queryCaches.GetOrAdd(
1030+
var wasAdded = false;
1031+
// The factory may be run concurrently by threads trying to get the same region.
1032+
// But the GetOrAdd will yield the same cache for all threads, so in case of add, use
1033+
// it to update allCacheRegions
1034+
var cache = queryCaches.GetOrAdd(
10311035
cacheRegion,
10321036
cr =>
10331037
{
1034-
IQueryCache currentQueryCache = settings.QueryCacheFactory.GetQueryCache(cacheRegion, updateTimestampsCache, settings, properties);
1035-
allCacheRegions[currentQueryCache.RegionName] = currentQueryCache.Cache;
1038+
var currentQueryCache = settings.QueryCacheFactory.GetQueryCache(cacheRegion, updateTimestampsCache, settings, properties);
1039+
wasAdded = true;
10361040
return currentQueryCache;
10371041
});
1042+
if (wasAdded)
1043+
allCacheRegions[cache.RegionName] = cache.Cache;
1044+
return cache;
10381045
}
10391046

10401047
public void EvictQueries()

0 commit comments

Comments
 (0)