Skip to content

Commit da429f7

Browse files
Fix cache build for honoring mapped concurrency (#1480)
Co-authored-by: Alexander Zaytsev <hazzik@gmail.com>
1 parent c0e9d73 commit da429f7

21 files changed

+574
-305
lines changed

src/NHibernate.Test/Async/CacheTest/GetQueryCacheFixture.cs renamed to src/NHibernate.Test/Async/CacheTest/BuildCacheFixture.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,28 @@
99

1010

1111
using System;
12-
using System.Collections;
1312
using System.Collections.Concurrent;
1413
using System.Collections.Generic;
1514
using System.Threading;
1615
using NHibernate.Cache;
1716
using NHibernate.Cfg;
17+
using NHibernate.Engine;
18+
using NHibernate.Util;
1819
using NUnit.Framework;
1920
using Environment = NHibernate.Cfg.Environment;
2021

2122
namespace NHibernate.Test.CacheTest
2223
{
2324
using System.Threading.Tasks;
2425
[TestFixture]
25-
public class GetQueryCacheFixtureAsync : TestCase
26+
public class BuildCacheFixtureAsync : TestCase
2627
{
27-
protected override string[] Mappings => new[] { "Simple.hbm.xml" };
28+
protected override string MappingsAssembly => "NHibernate.Test";
29+
30+
protected override string[] Mappings => new[] { "CacheTest.EntitiesInSameRegion.hbm.xml" };
31+
32+
// Disable the TestCase cache overrides.
33+
protected override string CacheConcurrencyStrategy => null;
2834

2935
protected override void Configure(Configuration configuration)
3036
{
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Threading;
5+
using NHibernate.Cache;
6+
using NHibernate.Cfg;
7+
using NHibernate.Engine;
8+
using NHibernate.Util;
9+
using NUnit.Framework;
10+
using Environment = NHibernate.Cfg.Environment;
11+
12+
namespace NHibernate.Test.CacheTest
13+
{
14+
[TestFixture]
15+
public class BuildCacheFixture : TestCase
16+
{
17+
protected override string MappingsAssembly => "NHibernate.Test";
18+
19+
protected override string[] Mappings => new[] { "CacheTest.EntitiesInSameRegion.hbm.xml" };
20+
21+
// Disable the TestCase cache overrides.
22+
protected override string CacheConcurrencyStrategy => null;
23+
24+
protected override void Configure(Configuration configuration)
25+
{
26+
configuration.SetProperty(Environment.UseQueryCache, "true");
27+
configuration.SetProperty(Environment.CacheProvider, typeof(LockedCacheProvider).AssemblyQualifiedName);
28+
}
29+
30+
[Theory]
31+
public void CommonRegionHasOneUniqueCacheAndExpectedConcurrency(bool withPrefix)
32+
{
33+
const string prefix = "Prefix";
34+
const string region = "Common";
35+
var fullRegion = (withPrefix ? prefix + "." : "") + region;
36+
ISessionFactoryImplementor sfi = null;
37+
if (withPrefix)
38+
cfg.SetProperty(Environment.CacheRegionPrefix, prefix);
39+
try
40+
{
41+
sfi = withPrefix ? BuildSessionFactory() : Sfi;
42+
var commonRegionCache = sfi.GetSecondLevelCacheRegion(fullRegion);
43+
var entityAName = typeof(EntityA).FullName;
44+
var entityAConcurrencyCache = sfi.GetEntityPersister(entityAName).Cache;
45+
var entityACache = entityAConcurrencyCache.Cache;
46+
var entityBName = typeof(EntityB).FullName;
47+
var entityBConcurrencyCache = sfi.GetEntityPersister(entityBName).Cache;
48+
var entityBCache = entityBConcurrencyCache.Cache;
49+
var relatedAConcurrencyCache =
50+
sfi.GetCollectionPersister(StringHelper.Qualify(entityAName, nameof(EntityA.Related))).Cache;
51+
var relatedACache = relatedAConcurrencyCache.Cache;
52+
var relatedBConcurrencyCache =
53+
sfi.GetCollectionPersister(StringHelper.Qualify(entityBName, nameof(EntityB.Related))).Cache;
54+
var relatedBCache = relatedBConcurrencyCache.Cache;
55+
var queryCache = sfi.GetQueryCache(region).Cache;
56+
Assert.Multiple(
57+
() =>
58+
{
59+
Assert.That(commonRegionCache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for common region");
60+
Assert.That(entityACache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for EntityA");
61+
Assert.That(entityBCache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for EntityB");
62+
Assert.That(relatedACache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for RelatedA");
63+
Assert.That(relatedBCache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for RelatedB");
64+
Assert.That(queryCache.RegionName, Is.EqualTo(fullRegion), "Unexpected region name for query cache");
65+
});
66+
Assert.Multiple(
67+
() =>
68+
{
69+
Assert.That(entityAConcurrencyCache, Is.InstanceOf<ReadWriteCache>(), "Unexpected concurrency for EntityA");
70+
Assert.That(relatedAConcurrencyCache, Is.InstanceOf<NonstrictReadWriteCache>(), "Unexpected concurrency for RelatedA");
71+
Assert.That(entityBConcurrencyCache, Is.InstanceOf<ReadOnlyCache>(), "Unexpected concurrency for EntityB");
72+
Assert.That(relatedBConcurrencyCache, Is.InstanceOf<ReadWriteCache>(), "Unexpected concurrency for RelatedB");
73+
Assert.That(entityACache, Is.SameAs(commonRegionCache), "Unexpected cache for EntityA");
74+
Assert.That(entityBCache, Is.SameAs(commonRegionCache), "Unexpected cache for EntityB");
75+
Assert.That(relatedACache, Is.SameAs(commonRegionCache), "Unexpected cache for RelatedA");
76+
Assert.That(relatedBCache, Is.SameAs(commonRegionCache), "Unexpected cache for RelatedB");
77+
Assert.That(queryCache, Is.SameAs(commonRegionCache), "Unexpected cache for query cache");
78+
});
79+
}
80+
finally
81+
{
82+
if (withPrefix)
83+
{
84+
cfg.Properties.Remove(Environment.CacheRegionPrefix);
85+
sfi?.Dispose();
86+
}
87+
}
88+
}
89+
90+
[Test]
91+
public void RetrievedQueryCacheMatchesGloballyStoredOne()
92+
{
93+
var region = "RetrievedQueryCacheMatchesGloballyStoredOne";
94+
LockedCache.Semaphore = new SemaphoreSlim(0);
95+
LockedCache.CreationCount = 0;
96+
try
97+
{
98+
var failures = new ConcurrentBag<Exception>();
99+
var thread1 = new Thread(
100+
() =>
101+
{
102+
try
103+
{
104+
Sfi.GetQueryCache(region);
105+
}
106+
catch (Exception e)
107+
{
108+
failures.Add(e);
109+
}
110+
});
111+
var thread2 = new Thread(
112+
() =>
113+
{
114+
try
115+
{
116+
Sfi.GetQueryCache(region);
117+
}
118+
catch (Exception e)
119+
{
120+
failures.Add(e);
121+
}
122+
});
123+
thread1.Start();
124+
thread2.Start();
125+
126+
// Give some time to threads for reaching the wait, having all of them ready to do most of their job concurrently.
127+
Thread.Sleep(100);
128+
// Let only one finish its job, it should be the one being stored in query cache dictionary.
129+
LockedCache.Semaphore.Release(1);
130+
// Give some time to released thread to finish its job.
131+
Thread.Sleep(100);
132+
// Release other thread
133+
LockedCache.Semaphore.Release(10);
134+
135+
thread1.Join();
136+
thread2.Join();
137+
Assert.That(failures, Is.Empty, $"{failures.Count} thread(s) failed.");
138+
}
139+
finally
140+
{
141+
LockedCache.Semaphore.Dispose();
142+
LockedCache.Semaphore = null;
143+
}
144+
145+
var queryCache = Sfi.GetQueryCache(region).Cache;
146+
var globalCache = Sfi.GetSecondLevelCacheRegion(region);
147+
Assert.That(globalCache, Is.SameAs(queryCache));
148+
Assert.That(LockedCache.CreationCount, Is.EqualTo(1));
149+
}
150+
}
151+
152+
public class LockedCache : HashtableCache
153+
{
154+
public static SemaphoreSlim Semaphore { get; set; }
155+
156+
private static int _creationCount;
157+
158+
public static int CreationCount
159+
{
160+
get => _creationCount;
161+
set => _creationCount = value;
162+
}
163+
164+
public LockedCache(string regionName) : base(regionName)
165+
{
166+
Semaphore?.Wait();
167+
Interlocked.Increment(ref _creationCount);
168+
}
169+
}
170+
171+
public class LockedCacheProvider : ICacheProvider
172+
{
173+
// Since 5.2
174+
[Obsolete]
175+
ICache ICacheProvider.BuildCache(string regionName, IDictionary<string, string> properties)
176+
{
177+
return BuildCache(regionName, properties);
178+
}
179+
180+
public CacheBase BuildCache(string regionName, IDictionary<string, string> properties)
181+
{
182+
return new LockedCache(regionName);
183+
}
184+
185+
public long NextTimestamp()
186+
{
187+
return Timestamper.Next();
188+
}
189+
190+
public void Start(IDictionary<string, string> properties)
191+
{
192+
}
193+
194+
public void Stop()
195+
{
196+
}
197+
}
198+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Collections.Generic;
2+
3+
namespace NHibernate.Test.CacheTest
4+
{
5+
public class EntityInSameRegion
6+
{
7+
public virtual int Id { get; set; }
8+
public virtual string Description { get; set; }
9+
public virtual int Value { get; set; }
10+
public virtual ISet<EntityInSameRegion> Related { get; set; }
11+
}
12+
13+
public class EntityA : EntityInSameRegion
14+
{
15+
}
16+
17+
public class EntityB : EntityInSameRegion
18+
{
19+
}
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
3+
assembly="NHibernate.Test"
4+
namespace="NHibernate.Test.CacheTest">
5+
<class name="EntityA">
6+
<cache usage="read-write" region="Common" />
7+
<id type="int" />
8+
<property name="Description"/>
9+
<property name="Value" column="`Value`"/>
10+
<set name="Related">
11+
<cache usage="nonstrict-read-write" region="Common" />
12+
<key column="Related" />
13+
<one-to-many class="EntityA"/>
14+
</set>
15+
</class>
16+
<class name="EntityB">
17+
<cache usage="read-only" region="Common" />
18+
<id type="int" />
19+
<property name="Description"/>
20+
<property name="Value" column="`Value`"/>
21+
<set name="Related">
22+
<cache usage="read-write" region="Common" />
23+
<key column="Related" />
24+
<one-to-many class="EntityB"/>
25+
</set>
26+
</class>
27+
<query name="EntityA.All" cache-region="Common" cacheable="true">
28+
from EntityA
29+
</query>
30+
</hibernate-mapping>

0 commit comments

Comments
 (0)