Skip to content

Commit db76861

Browse files
committed
Add tests for NH-3050 contributed by Christophe Gijbels
1 parent 2a4041e commit db76861

File tree

4 files changed

+189
-0
lines changed

4 files changed

+189
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Reflection;
5+
using System.Threading;
6+
7+
using NHibernate.Engine.Query;
8+
using NHibernate.Linq;
9+
using NHibernate.Util;
10+
11+
using NUnit.Framework;
12+
using System.Linq;
13+
14+
namespace NHibernate.Test.NHSpecificTest.NH3050
15+
{
16+
[TestFixture]
17+
public class Fixture : BugTestCase
18+
{
19+
[Test]
20+
public void Test()
21+
{
22+
// WARNING: This test case makes use of reflection resulting in failures if internals change, but reflection was needed to allow for simulation.
23+
// This test simulates a heavy load on the QueryPlanCache by making it use a SoftLimitMRUCache instance with a size of 1 instead of the default of 128.
24+
// Since this cache moves the most recently used query plans to the top, pushing down the less used query plans until they are removed from the cache,
25+
// the smaller the size of the cache the sooner the query plans will be dropped, making it easier to simulate the problem.
26+
//
27+
// What is the exact problem:
28+
// -> When executing a LINQ query that has a contains with only one element in the collection the same queryExpression string is generated by 2 different types
29+
// of IQueryExpression, the 'NHibernate.Impl.ExpandedQueryExpression' and the 'NHibernate.Linq.NhLinqExpression' and that key is used to store a query plan
30+
// in the QueryPlanCache if a query plan is requested and not found in the cache.
31+
// -> The 'NHibernate.Linq.NhLinqExpression' is typically added during the DefaultQueryProvider.PrepareQuery and the 'NHibernate.Impl.ExpandedQueryExpression'
32+
// less likely during the execution of the LINQ query
33+
// -> Unfortunately the PrepareQuery is casting the returned query plan's QueryExpression to a NhLinqExpression, which it assumes will always be the case, but this
34+
// is not true in a heavy loaded environment where the cache entries are constantly moving when other queries are being executed at the same time.
35+
// -> If you look at the following method inside the DefaultQueryProvider class, then you'll see that by drilling down in the PrepareQuery and in the ExecuteQuery, that
36+
// both operations are actually requesting the query plan from the QueryPlanCache at some point
37+
// public virtual object Execute(Expression expression)
38+
// {
39+
// IQuery query;
40+
// NhLinqExpression nhQuery;
41+
// NhLinqExpression nhLinqExpression = PrepareQuery(expression, out query, out nhQuery);
42+
// return ExecuteQuery(nhLinqExpression, query, nhQuery);
43+
// }
44+
//
45+
// When they are requesting the corresponding query plan according to the QueryExpression's key the PrepareQuery assumes it will get back a NhLinqExpression, while it
46+
// is perfectly possible that the corresponding query plan has a QueryExpression of type ExpandedQueryExpression that has been added during the ExecuteQuery because
47+
// when a request was made for the query plan during the execution, the load on the cache has put the query plan with a QueryExpression of type NhLinqExpression and with
48+
// the same key somewhere at the bottom of the MRU cache and it might even have been removed from the cache, resulting in adding a query plan with a QueryExpression value
49+
// of type ExpandedQueryExpression. When the same LINQ query is executed afterwards, it will go through the PrepareQuery again, assuming that what is returned is a
50+
// NhLinqExpression, while in reality it is an ExpandedQueryExpression, resulting in a cast exception. This problem might even go away due to the same load, pushing out
51+
// the cached query plan with a QueryExpression of ExpandedQueryExpression and have a NhLinqExpression added back again during the next Prepare.
52+
//
53+
// So this test will simulate the pushing out by clearing the cache as long as the QueryExpression of the query plan is NhLinqExpression, once it is an ExpandedQueryExpression
54+
// it will stop clearing the cache, and the exception will occur, resulting in a failure of the test.
55+
// The test will pass once all LINQ expression are executed (1000 max) and no exception occured
56+
57+
var cache = new SoftLimitMRUCache(1);
58+
59+
var queryPlanCacheType = typeof (QueryPlanCache);
60+
61+
// get the planCache field on the QueryPlanCache and overwrite it with the restricted cache
62+
queryPlanCacheType
63+
.GetField("planCache", BindingFlags.Instance | BindingFlags.NonPublic)
64+
.SetValue(sessions.QueryPlanCache, cache);
65+
66+
// Initiate a LINQ query with a contains with one item in it, of which we know that the underlying IQueryExpression implementations
67+
// aka NhLinqExpression and the ExpandedQueryExpression generate the same key.
68+
IEnumerable<int> personIds = new List<int>
69+
{
70+
1
71+
};
72+
73+
ISession session = null;
74+
75+
try
76+
{
77+
session = OpenSession();
78+
79+
var allLinqQueriesSucceeded = false;
80+
81+
// Setup an action delegate that will be executed on a separate thread and that will execute the LINQ query above multiple times.
82+
// This will constantly interact with the cache (Once in the PrepareQuery method of the DefaultQueryProvider and once in the Execute)
83+
System.Action queryExecutor = () =>
84+
{
85+
var sessionToUse = sessions.OpenSession();
86+
87+
try
88+
{
89+
for (var i = 0; i < 1000; i++)
90+
{
91+
(from person in session.Query<Person>()
92+
where personIds.Contains(person.Id)
93+
select person).ToList();
94+
}
95+
96+
allLinqQueriesSucceeded = true;
97+
}
98+
finally
99+
{
100+
if (sessionToUse != null && sessionToUse.IsOpen)
101+
{
102+
sessionToUse.Close();
103+
}
104+
}
105+
};
106+
107+
(from person in session.Query<Person>()
108+
where personIds.Contains(person.Id)
109+
select person).ToList();
110+
111+
// the planCache now contains one item with a key of type HQLQueryPlanKey,
112+
// so we are going to retrieve the generated key so that we can use it afterwards to interact with the cache.
113+
// The softReferenceCache field value from the SoftLimitMRUCache cache instance contains this key
114+
var field = cache.GetType().GetField("softReferenceCache", BindingFlags.NonPublic | BindingFlags.Instance);
115+
116+
var softReferenceCache = (IEnumerable) field.GetValue(cache);
117+
118+
// Since the cache only contains one item, the first one will be our key
119+
var queryPlanCacheKey = ((DictionaryEntry) softReferenceCache.First()).Key;
120+
121+
// Setup an action that will be run on another thread and that will do nothing more than clearing the cache as long
122+
// as the value stored behind the cachekey is not of type ExpandedQueryExpression, which triggers the error.
123+
// By running this constantly in concurrency with the thread executing the query, the odds of having the wrong
124+
// QueryExpression in the cache (wrong as in the PrepareQuery is not expecting it) augments, simulating the workings
125+
// of the MRU algorithm under load.
126+
System.Action cacheCleaner = () =>
127+
{
128+
while (!allLinqQueriesSucceeded)
129+
{
130+
var hqlExpressionQueryPlan = (HQLExpressionQueryPlan) cache[queryPlanCacheKey];
131+
if (hqlExpressionQueryPlan != null)
132+
{
133+
if (hqlExpressionQueryPlan.QueryExpression.GetType().FullName.Contains("NHibernate.Impl.ExpandedQueryExpression"))
134+
{
135+
// we'll stop clearing the cache, since the cache now has a different query expression type than expected by the code
136+
break;
137+
}
138+
}
139+
140+
cache.Clear();
141+
142+
// we sleep a little, just to make sure the cache is not constantly empty ;-)
143+
Thread.Sleep(50);
144+
}
145+
};
146+
147+
var queryExecutorAsyncResult = queryExecutor.BeginInvoke(null, null);
148+
var cacheCleanerAsyncResult = cacheCleaner.BeginInvoke(null, null);
149+
150+
queryExecutor.EndInvoke(queryExecutorAsyncResult);
151+
cacheCleaner.EndInvoke(cacheCleanerAsyncResult);
152+
153+
Assert.IsTrue(allLinqQueriesSucceeded);
154+
}
155+
finally
156+
{
157+
if (session != null)
158+
{
159+
session.Close();
160+
}
161+
}
162+
}
163+
}
164+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
3+
namespace="NHibernate.Test.NHSpecificTest.NH3050"
4+
assembly="NHibernate.Test">
5+
6+
<class name="Person">
7+
<id name="Id">
8+
<generator class="native"/>
9+
</id>
10+
</class>
11+
12+
</hibernate-mapping>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
3+
namespace NHibernate.Test.NHSpecificTest.NH3050
4+
{
5+
[Serializable]
6+
public class Person
7+
{
8+
public virtual int Id { get; set; }
9+
}
10+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,8 @@
665665
<Compile Include="NHSpecificTest\BagWithLazyExtraAndFilter\Domain.cs" />
666666
<Compile Include="NHSpecificTest\BagWithLazyExtraAndFilter\Fixture.cs" />
667667
<Compile Include="Component\Basic\ComponentWithUniqueConstraintTests.cs" />
668+
<Compile Include="NHSpecificTest\NH3050\Fixture.cs" />
669+
<Compile Include="NHSpecificTest\NH3050\Person.cs" />
668670
<Compile Include="NHSpecificTest\NH2469\Domain.cs" />
669671
<Compile Include="NHSpecificTest\NH2469\Fixture.cs" />
670672
<Compile Include="NHSpecificTest\NH2033\Customer.cs" />
@@ -2904,6 +2906,7 @@
29042906
<EmbeddedResource Include="NHSpecificTest\NH1291AnonExample\Mappings.hbm.xml" />
29052907
</ItemGroup>
29062908
<ItemGroup>
2909+
<EmbeddedResource Include="NHSpecificTest\NH3050\Mappings.hbm.xml" />
29072910
<EmbeddedResource Include="NHSpecificTest\NH2469\Mappings.hbm.xml" />
29082911
<EmbeddedResource Include="NHSpecificTest\NH2033\Mappings.hbm.xml" />
29092912
<EmbeddedResource Include="NHSpecificTest\NH2819\Mappings.hbm.xml" />

0 commit comments

Comments
 (0)