-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Thread-safety NullPointerException fix for EntityEntryContext #11261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Thanks for your pull request! This pull request does not follow the contribution rules. Could you have a look? ❌ All commit messages should start with a JIRA issue key matching pattern › This message was automatically generated. |
mbellade
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@werner77 note that various aspects of Hibernate's session are not thread safe, so if you're sharing the same instance between multiple threads you will run into more problems than just this NPE.
Not saying we won't accept this set of changes, reducing the number of times we invoke methods from the ManagedEntity interface is desirable anyway to avoid virtual call sites when using bytecode enhanced entities.
You seem to have based your changes on a very old version of the code though, please rebase on the current branch.
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Outdated
Show resolved
Hide resolved
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Outdated
Show resolved
Hide resolved
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Outdated
Show resolved
Hide resolved
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Outdated
Show resolved
Hide resolved
Let's be much clearer about this: as is extensively documented, the session is not ever threadsafe, and you should never hit the session from more than one thread.
I don't think we should accept it as-is. Just changing stuff to return |
| final EntityEntry entry = managedEntity.$$_hibernate_getEntityEntry(); | ||
| if( entry == null ) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is a condition that can only occur as a result of access to a session by concurrent threads, return true is not appropriate. It's better to throw.
| final EntityEntry entry = managedEntity.$$_hibernate_getEntityEntry(); | |
| if( entry == null ) { | |
| return true; | |
| } | |
| final EntityEntry entry = managedEntity.$$_hibernate_getEntityEntry(); | |
| if( entry == null ) { | |
| throw new AssertionError("Probable multithreaded access to session"); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is old code, but the null case was already there and has nothing to do with concurrency. This is irrelevant anyway as in current main this method has been rewritten.
|
I rebased the branch. Besides any threading issues I think the null safety can be addressed more properly as proposed. As far as I am aware though I am never sharing any sessions between multiple threads. I don't use Hibernate sessions directly, I am always interacting with them through an EntityManager proxy provided by Spring framework. However, throwing an assertion error between two successive calls of the same property does not seem like the right place to guard against multi-threading? I guess that should be done at a more central place or guard all such invocations in a central manner? |
Hibernate itself cannot possibly to anything useful to guard against incorrect use of the session by your program or by Spring, because Hibernate is not in control of the thread pool, nor of allocation of work to the thread pool. The best we can do is throw assertion errors if we detect something inconsistent in our internal state. |
|
I see the problematic pattern. I don't know if this is allowed by Hibernate or not: schematically: I use this pattern to hand over entities to a different thread. The entityManager.contains is used as part of a generic utility method called attach where I ensure the entities are properly attached to the context. |
Asynchronicity isn't completely disallowed. But you would have to ensure that any asynchronous process which accesses the session has exclusive access to the session from the point it is forked until the point it completes. After the asynchronous process completes, then some other process could in principle regain access to the session, but not before. |
|
Just for me to understand. So it is not allowed to hand an entity from one EntityManager to a different thread, having its own EntityManager and calling EntityManager.contains() in that other thread? Because that is exactly the use case where the exception occurs. The EntityManagers above (which wrap the Hibernate sessions) are distinct so NOT reused in different threads (they are injected by Spring framework via its thread safe proxy). The problem arises because the managed entity itself has mutable state which is introspected before even the persistence context is compared in the contains() call. |
This is allowed. The
Very skeptical that this could possibly be true.
WDYM by this? "Introspected" by what code written by who?
|
|
Stack trace is as follows (according to my understanding):
Because there are two consecutive calls to the last method the result can change between those calls causing the issue. Only later on this comparison is made: entityEntry.getPersistenceContext() == persistenceContext This would always return false for two different sessions. However, the NullPointerException can occur before that. |
|
Well then that just sounds like a bug in the |
|
Ok than either contains() should not eventually call entityEntryContext.getAssociatedManagedEntity() (which seems unlikely) or it should protect with locks, or the NullPointerException should be fixed as above without locking. I guess the latter is the cleanest. It won't have any negative side-effects because it does not change the semantics of the code, but only reduces some duplicated calls. Am I missing anything? |
|
I have no idea, because I have no way to reproduce the problem. |
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Show resolved
Hide resolved
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Show resolved
Hide resolved
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Outdated
Show resolved
Hide resolved
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Show resolved
Hide resolved
|
So you have proposed four changes. Two are perfectly unobjectionable, and should be done anyway, as @mbellade said earlier. The other two look pretty suspicious and I don't see why they should be on the code path of the |
|
I don't know how to create the proper unit test in the Hibernate source code, but I created a unit test in our code base: This test crashes with the NullPointerException as described: |
|
I removed the two changes which were not necessary/problematic |
mbellade
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good, one minor nitpick but other than that this is fine.
hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java
Outdated
Show resolved
Hide resolved
|
Thanks @werner77, please squash everything to a single commit |
This fixes a NullPointerException we observe sometimes in our error reporting system.
Specifically because the calls to $$_hibernate_getEntityEntry() are duplicated and this resolves to a mutable field.
The NullPointerException stack trace we observe is:
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license
and can be relicensed under the terms of the LGPL v2.1 license in the future at the maintainers' discretion.
For more information on licensing, please check here.