public void Initialize(IDictionary<string, string> properties) { int size = 128; if (properties.ContainsKey(SIZE)) { if (!int.TryParse(properties[SIZE], out size)) { // TODO: Log a warning size = 128; } } cache = new SoftLimitMRUCache(size); }
public void DontFillUp() { // NH-1671 const int count = 32; var s = new SoftLimitMRUCache(count); for (int i = 0; i < count+10; i++) { s.Put(new object(), new object()); } Assert.That(s.Count, Is.EqualTo(count)); GC.Collect(); s.Put(new object(), new object()); Assert.That(s.SoftCount, Is.EqualTo(count + 1)); }
public void Test() { // WARNING: This test case makes use of reflection resulting in failures if internals change, but reflection was needed to allow for simulation. // 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. // 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, // the smaller the size of the cache the sooner the query plans will be dropped, making it easier to simulate the problem. // // What is the exact problem: // -> 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 // of IQueryExpression, the 'NHibernate.Impl.ExpandedQueryExpression' and the 'NHibernate.Linq.NhLinqExpression' and that key is used to store a query plan // in the QueryPlanCache if a query plan is requested and not found in the cache. // -> The 'NHibernate.Linq.NhLinqExpression' is typically added during the DefaultQueryProvider.PrepareQuery and the 'NHibernate.Impl.ExpandedQueryExpression' // less likely during the execution of the LINQ query // -> Unfortunately the PrepareQuery is casting the returned query plan's QueryExpression to a NhLinqExpression, which it assumes will always be the case, but this // 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. // -> 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 // both operations are actually requesting the query plan from the QueryPlanCache at some point // public virtual object Execute(Expression expression) // { // IQuery query; // NhLinqExpression nhQuery; // NhLinqExpression nhLinqExpression = PrepareQuery(expression, out query, out nhQuery); // return ExecuteQuery(nhLinqExpression, query, nhQuery); // } // // 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 // is perfectly possible that the corresponding query plan has a QueryExpression of type ExpandedQueryExpression that has been added during the ExecuteQuery because // 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 // 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 // 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 // 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 // the cached query plan with a QueryExpression of ExpandedQueryExpression and have a NhLinqExpression added back again during the next Prepare. // // 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 // it will stop clearing the cache, and the exception will occur, resulting in a failure of the test. // The test will pass once all LINQ expression are executed (1000 max) and no exception occured var cache = new SoftLimitMRUCache(1); var queryPlanCacheType = typeof (QueryPlanCache); // get the planCache field on the QueryPlanCache and overwrite it with the restricted cache queryPlanCacheType .GetField("planCache", BindingFlags.Instance | BindingFlags.NonPublic) .SetValue(sessions.QueryPlanCache, cache); // Initiate a LINQ query with a contains with one item in it, of which we know that the underlying IQueryExpression implementations // aka NhLinqExpression and the ExpandedQueryExpression generate the same key. IEnumerable<int> personIds = new List<int> { 1 }; ISession session = null; try { session = OpenSession(); var allLinqQueriesSucceeded = false; // Setup an action delegate that will be executed on a separate thread and that will execute the LINQ query above multiple times. // This will constantly interact with the cache (Once in the PrepareQuery method of the DefaultQueryProvider and once in the Execute) System.Action queryExecutor = () => { var sessionToUse = sessions.OpenSession(); try { for (var i = 0; i < 1000; i++) { (from person in session.Query<Person>() where personIds.Contains(person.Id) select person).ToList(); } allLinqQueriesSucceeded = true; } finally { if (sessionToUse != null && sessionToUse.IsOpen) { sessionToUse.Close(); } } }; (from person in session.Query<Person>() where personIds.Contains(person.Id) select person).ToList(); // the planCache now contains one item with a key of type HQLQueryPlanKey, // so we are going to retrieve the generated key so that we can use it afterwards to interact with the cache. // The softReferenceCache field value from the SoftLimitMRUCache cache instance contains this key var field = cache.GetType().GetField("softReferenceCache", BindingFlags.NonPublic | BindingFlags.Instance); var softReferenceCache = (IEnumerable) field.GetValue(cache); // Since the cache only contains one item, the first one will be our key var queryPlanCacheKey = ((DictionaryEntry) softReferenceCache.First()).Key; // Setup an action that will be run on another thread and that will do nothing more than clearing the cache as long // as the value stored behind the cachekey is not of type ExpandedQueryExpression, which triggers the error. // By running this constantly in concurrency with the thread executing the query, the odds of having the wrong // QueryExpression in the cache (wrong as in the PrepareQuery is not expecting it) augments, simulating the workings // of the MRU algorithm under load. System.Action cacheCleaner = () => { while (!allLinqQueriesSucceeded) { var hqlExpressionQueryPlan = (HQLExpressionQueryPlan) cache[queryPlanCacheKey]; if (hqlExpressionQueryPlan != null) { if (hqlExpressionQueryPlan.QueryExpression.GetType().FullName.Contains("NHibernate.Impl.ExpandedQueryExpression")) { // we'll stop clearing the cache, since the cache now has a different query expression type than expected by the code break; } } cache.Clear(); // we sleep a little, just to make sure the cache is not constantly empty ;-) Thread.Sleep(50); } }; var queryExecutorAsyncResult = queryExecutor.BeginInvoke(null, null); var cacheCleanerAsyncResult = cacheCleaner.BeginInvoke(null, null); queryExecutor.EndInvoke(queryExecutorAsyncResult); cacheCleaner.EndInvoke(cacheCleanerAsyncResult); Assert.IsTrue(allLinqQueriesSucceeded); } finally { if (session != null) { session.Close(); } } }