Session leak in NHLinq Query Plan Cache

433 views
Skip to first unread message

Lauri Kotilainen

unread,
Dec 11, 2013, 4:52:43 PM12/11/13
to nhibernate-...@googlegroups.com
Hi,

I originally posted the description of this issue to the nhusers list:

https://groups.google.com/d/topic/nhusers/v_6WCod79XE/discussion

and I won't waste bits by pasting the entire description here unless it's deemed necessary. Anyway, I think I have a patch that fixes the session leak, but I don't understand the big picture well enough to evaluate whether or not it's a safe change. Essentially, what I did was move most of the code from NhLinqExpression.Translate to the NhLinqExpression constructor and eliminated the _expression field, making it a local variable in the ctor. It didn't cause any test failures in the master branch, and after I backported it to the 3.4.x branch and tested with the problematic application, the leak is gone (according to ANTS profiler).

Here's the patch:

Unfortunately, even with that patch applied, the unit test in my other post fails -- even though a heap inspection with WinDBG confirms that the session is no longer rooted and is eligible for collection.

What I would like to know at this point is whether the change is likely to cause performance regressions or other unexpected side effects.

Thanks,

-Lauri

Gunnar Liljas

unread,
Dec 11, 2013, 7:03:21 PM12/11/13
to nhibernate-development
Great work, Lauri!

I'll do some tests tomorrow, just to give you feedback.

/G



2013/12/11 Lauri Kotilainen <ryt...@gmail.com>

--
 
---
You received this message because you are subscribed to the Google Groups "nhibernate-development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to nhibernate-develo...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.

Gunnar Liljas

unread,
Dec 13, 2013, 7:41:36 AM12/13/13
to nhibernate-development
So far it looks OK, although it's a bit troubling that the query plan cache does this. I'm not a big fan of heavy constructors, but in this case it makes some sense. 

I think you can make your test behave correctly (i.e fail when it should) by adding a second pass of GC.Collect()

A JIRA issue and an accompanying unit test (for now, remove the dependency on SQLite) would be nice.

/G


2013/12/12 Gunnar Liljas <gunnar...@gmail.com>

Lauri Kotilainen

unread,
Dec 15, 2013, 8:29:24 AM12/15/13
to nhibernate-...@googlegroups.com
Which part is troubling? The part about the leak or the part about having the query plan cache end up constructing the expression?

If it's the latter, I did try other approaches too. I don't really like heavy ctors either, but the QPC and NhLinqExpressions are used in a variety of ways, and all the alternatives I could think of ended up causing breakage all over the place. This might be due to the fact that I don't have a very good mental map of the dependencies yet, though.

I'll file an issue soon-ish with the changes you proposed to the test. 

-Lauri


2013/12/12 Gunnar Liljas <gunnar...@gmail.com>


2013/12/11 Lauri Kotilainen <ryt...@gmail.com>
To unsubscribe from this group and stop receiving emails from it, send an email to nhibernate-development+unsub...@googlegroups.com.

Gunnar Liljas

unread,
Dec 16, 2013, 6:15:21 AM12/16/13
to nhibernate-development
It's troubling that the leak occurred in the first place.

/G


2013/12/15 Lauri Kotilainen <ryt...@gmail.com>
To unsubscribe from this group and stop receiving emails from it, send an email to nhibernate-develo...@googlegroups.com.

Lauri Kotilainen

unread,
Dec 16, 2013, 6:35:41 AM12/16/13
to nhibernate-...@googlegroups.com
Yeah... an actual case of "select is broken". Who would have thought.

Jira issue:

Pull request:

I based the patch off the master branch -- figured that someone else will be a better judge of what to backport to earlier versions.

-Lauri


2013/12/15 Lauri Kotilainen <ryt...@gmail.com>


2013/12/12 Gunnar Liljas <gunnar...@gmail.com>


2013/12/11 Lauri Kotilainen <ryt...@gmail.com>

To unsubscribe from this group and stop receiving emails from it, send an email to nhibernate-development+unsubscri...@googlegroups.com.

For more options, visit https://groups.google.com/groups/opt_out.

Роман Квасов

unread,
Jan 10, 2014, 7:07:50 AM1/10/14
to nhibernate-...@googlegroups.com
Hello,

I too was hit by this bug.
Fix looks simple but it lay month without any activity.
Can I somehow help with this merge request?


2013/12/16 Lauri Kotilainen <ryt...@gmail.com>
To unsubscribe from this group and stop receiving emails from it, send an email to nhibernate-develo...@googlegroups.com.

Lauri Kotilainen

unread,
Jan 10, 2014, 8:29:49 AM1/10/14
to nhibernate-...@googlegroups.com
I can't speak for the core developer team, but I suppose you could try building the branch and smoke-testing it to see if it breaks anything. :)

-Lauri

Oskar Berggren

unread,
Jan 18, 2014, 2:22:29 PM1/18/14
to nhibernate-...@googlegroups.com

NhLinqExpression is instantiated in DefaultQueryProvider.PrepareQuery():

    protected NhLinqExpression PrepareQuery(Expression expression, out IQuery query, out NhLinqExpression nhQuery)
    {
        var nhLinqExpression = new NhLinqExpression(expression, Session.Factory);
        query = Session.CreateQuery(nhLinqExpression);
        ...

As I understand the code, it is the call to CreateQuery() that will lead to a query plan cache (QPC) lookup, and if no cached plan is found it will eventually call NhLinqExpression.Translate() before CreateQuery() returns. The proposed change, moves work from Translate() to the NhLinqExpression ctor. Unfortunately this effectively nullifies a large part of the purpose with the QPC, by having NH do the entire linq-to-hql-ast translation even before looking in the QPC.

If I'm right about the above, a simple change might be to just have the Translate() method nullify the _expression variable after it's done with it (and have it skip the translation code if ExpressionToHqlTranslationResults is already non-null).

Comments on this?

(On a somewhat tangential point, I feel the entire interaction between the linq implementation, QPC and the session implementation is rather messy code...)

/Oskar


2013/12/16 Lauri Kotilainen <ryt...@gmail.com>
To unsubscribe from this group and stop receiving emails from it, send an email to nhibernate-develo...@googlegroups.com.

Oskar Berggren

unread,
Jan 18, 2014, 4:44:53 PM1/18/14
to nhibernate-...@googlegroups.com

2013/12/11 Lauri Kotilainen <ryt...@gmail.com>

Here's the patch:

Unfortunately, even with that patch applied, the unit test in my other post fails -- even though a heap inspection with WinDBG confirms that the session is no longer rooted and is eligible for collection.

My attempts with dubious knowledge of Windbg seems to confirm that the test itself did indeed maintain a reference to the SessionImpl instance, as a local variable in the stack fram of IsGarbageCollected<TObject>(ref TObject @object, Action<TObject> useObject). I don't know how to explain that, given that the test code attempted to nullify the reference.

Anyway, I've merged a restructured test that ensures any stack frame that might reference the SessionImpl instance expires before the GC collection.

/Oskar

Lauri Kotilainen

unread,
Jan 20, 2014, 3:27:57 AM1/20/14
to nhibernate-...@googlegroups.com


On Saturday, January 18, 2014 11:44:53 PM UTC+2, Oskar Berggren wrote:

Unfortunately, even with that patch applied, the unit test in my other post fails -- even though a heap inspection with WinDBG confirms that the session is no longer rooted and is eligible for collection.

My attempts with dubious knowledge of Windbg seems to confirm that the test itself did indeed maintain a reference to the SessionImpl instance, as a local variable in the stack fram of IsGarbageCollected<TObject>(ref TObject @object, Action<TObject> useObject). I don't know how to explain that, given that the test code attempted to nullify the reference.

The second GC.Collect() pass fixed that for me, like Gunnar suggested. I'll take another look at it soon-ish to see if your modified test still behaves the way I think it should.
 
My first attempt at a fix actually was to discard the _expression reference. However, that didn't work -- IIRC because the results of the translation maintained another reference to the expression. Changing that would have had a considerably bigger change footprint, one that I wasn't comfortable with.

Another approach I tried was to replace the stored expression with something that wouldn't hold a session reference. Unfortunately, that only led to failing queries, largely due to the same reasons.

This change seems difficult to get right, which rather strongly supports your opinion about the messiness...

-Lauri

Oskar Berggren

unread,
Jan 20, 2014, 10:48:48 AM1/20/14
to nhibernate-...@googlegroups.com

2014/1/20 Lauri Kotilainen <ryt...@gmail.com>



On Saturday, January 18, 2014 11:44:53 PM UTC+2, Oskar Berggren wrote:

Unfortunately, even with that patch applied, the unit test in my other post fails -- even though a heap inspection with WinDBG confirms that the session is no longer rooted and is eligible for collection.

My attempts with dubious knowledge of Windbg seems to confirm that the test itself did indeed maintain a reference to the SessionImpl instance, as a local variable in the stack fram of IsGarbageCollected<TObject>(ref TObject @object, Action<TObject> useObject). I don't know how to explain that, given that the test code attempted to nullify the reference.

The second GC.Collect() pass fixed that for me, like Gunnar suggested. I'll take another look at it soon-ish to see if your modified test still behaves the way I think it should.

I added a bunch more, plus some other GC.xxx() methods, didn't help.
 

 
My first attempt at a fix actually was to discard the _expression reference. However, that didn't work -- IIRC because the results of the translation maintained another reference to the expression. Changing that would have had a considerably bigger change footprint, one that I wasn't comfortable with.

I later tried that too and discovered that Translate() is actually called twice and apparently produces different output on the second go. I haven't yet figured out what input data (or how) is modified to achieve this effect.
 

/Oskar

Alexander I. Zaytsev

unread,
Jan 20, 2014, 5:06:36 PM1/20/14
to nhibernate-...@googlegroups.com
Hi all,

I've made a pull request with a fix.


Please review.

Best Regards, 
Alexander


2014/1/21 Oskar Berggren <oskar.b...@gmail.com>

Роман Квасов

unread,
Jan 24, 2014, 8:30:38 AM1/24/14
to nhibernate-...@googlegroups.com
Hi Alexander,

I think you forgot big chunk of leak, NhLinqExpression leak reference to constants that used in linq expression.
If linq query reference for object it end up in cache and object not released.
Please review.


2014/1/21 Alexander I. Zaytsev <haz...@gmail.com>

cremor

unread,
Mar 27, 2014, 3:44:40 AM3/27/14
to nhibernate-...@googlegroups.com
Hi Roman,

Is there still a leak in the latest master trunk even after the fix for NH-3579? If yes, please create a new Jira issue and GitHub pull request.


2014/1/21 Oskar Berggren <oskar.b...@gmail.com>
To unsubscribe from this group and stop receiving emails from it, send an email to nhibernate-development+unsub...@googlegroups.com.

For more options, visit https://groups.google.com/groups/opt_out.

--
 
---
You received this message because you are subscribed to the Google Groups "nhibernate-development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to nhibernate-development+unsub...@googlegroups.com.

Maarten

unread,
Jun 10, 2015, 9:30:10 AM6/10/15
to nhibernate-...@googlegroups.com
His test case still works, can anyone confirm?

Op donderdag 27 maart 2014 08:44:40 UTC+1 schreef cremor:
Reply all
Reply to author
Forward
0 new messages