-
Notifications
You must be signed in to change notification settings - Fork 137
[2.2.x] OPENJPA-2817: OpenJPA enhancer needs improved reentrancy #64
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Conversation
Will create a trunk PR after this has been reviewed. |
@jgrassel , for now I'm trying to see in which case we need that. Say me if I'm wrong but the scope is only about the javaagent (other cases - ie container deployment enhancement and build time enhancement - must be single threaded by construction today). Since all entities are loaded when the EMF is created it should also be single threaded until the EMF is lazily created and concurrently at runtime - which must be handled by the EMF then IMO. The comment is surprising though cause it means it only happens when loading openjpa itself - which kind of implies the temp loader should filter openjpa "runtime" instead of adding this boolean. So wonder if you are able to replicate the issue in a test (even abusing of countdownlatch or "sleep" in loadClass) to ensure we fix an issue instead of propagating dead code. Hope it makes sense. |
@rmannibucau This issue was brought to my awareness after Open Liberty's application classloader was updated to allow multi-threaded concurrency (even though concurrency was allowed after JDK 7, OpenLiberty only very recently updated its own application classloader to allow this). Presumably, this would also affect other application servers which are also updated to take advantage of that particular approach for performance improvement. I was not able to replicate this in a test environment, but my customer has confirmed that this resolved the symptoms caused by the old design approach (in the form of entity types not being class transformed). |
@jgrassel can you thread dump (even with a monkey patch) when the issue occurs? OpenLiberty is supposed to have the EMF ready during CDI boot (I know there is a chicken egg issue between cdi and jpa in the spec so can be a toggle thing in OpenLiberty) and this part is monothreaded so classes are either concurrently loaded before (which can be but would be weird and wouldn't use enhancement at deploy time properly) or it is when bypassing this part (not EE case?). |
True, OpenLiberty creates the EMF during application start (it has to, in order to register the Class Transformer [with the application classloader] before any of the persistent capable classes are loaded), but the transformation is invoked when the class is first loaded, which can happen at any time after the transformer has been registered. In the customer scenario, two concurrent threads triggered a classload on the same application classloader. Before the classloader perf-update, one thread would secure a lock on the classloader, while the second thread would get blocked, the class that it requested would be enhanced, and then it would unlock the classloader, which then the other thread would get its lock. Now, both threads can concurrently request a class load without one being blocked, so both can enter the transformer (enhancer) and the same time. One thread sets the (x) - The |
Now, I can try to come up with an example to demonstrate it, but I am on a very tight deadline, so I can agree to delay committing the update to trunk, but I'm going to insist on the example not being a gating requirement for integrating into 2.2.x. |
Signed-off-by: Joe Grassel <jgrassel@apache.org>
c82e026
to
fe80c98
Compare
(updated the commit to rename |
@jgrassel Hmm, a few things which makes this fix looking very weird to me:
So at the end it sounds like a bug in the classloader you use in openliberty so I would fix it there. BTW where is Albert's review? I can't see it it seems. |
I asked Albert via company internal slack to review my changes, he's not set up a link between github and apache yet.
|
However, because now the classloader isn't going to synchronize between threads, we can't make that assumption anymore, hence we need to rely on a marker that is independent of the thread -- a ThreadLocal instead of a boolean.
|
What the boolean - with a threadlocal or not - enables is to not enhance some classes - this is why sometimes we can have "missing metadata" error in some setup because if an enhancement triggers the load of another entity, this last one is not enhanced - depends the tmploader which can be the same as app loader when using an agent to speed up the execution/boot. FYI: dropping the boolean the build still works fine so guess we should work forward the exclusion list. What about something like that:
|
This has nothing to do with the temp classloader. The temp classloader itself doesn't have an enhancer registered to it, it only exists as a means for the persistence provider impl to load a class (in order to inspect it via reflection, though OpenJPA uses SERP for that business, I don't believe OpenJPA does much with the temp classloader that PersistenceUnitInfo can provide) without using the real application classloader (because once a class is loaded, that's it, the opportunity to enhance it has been missed.) |
Personally, I'm not a fan of the "exclude list", because that adds a lot of unnecessary processing time, as String comparing a jar with hundreds and even thousands of classes against a group of patterns is not going to come cheap. In an application server environment, as long as the jpa provider impl is not bundled with the application (that is, loaded by the same classloader that we register a class transformer with), then the reentrancy that the But like I've said many times now, with ClassLoaders that permit concurrent classloading (and thus concurrent enhancing), the boolean method falls short because it doesn't detect thread-scoped reentrancy. |
Okay, here is a maven project that will demonstrate the problem with an application server using openjpa. Couple of notes:
However, you can build and extract the ear application generated, and run it on an application server that will run with OpenJPA. The problem will only manifest itself if the application classloader allows for concurrent classloading and enhancement. Example output using Commercial Liberty with the ear generated by this application:
Note that with Eclipselink, which has a bytecode weaver that properly manages reentrancy, doesn't suffer from this problem:
|
And in the OpenJPA case above, here is a cut of the trace log file showing EntityA (thread 000000a2) being enhanced, but EntityC (thread 000000a3) is not:
Notice we only see: |
I've now read the SE7 documentation about a few times and I think the ThreadLocal is not enough for this specific case. From the ClassLoader doc it appears that the lock now got moved from a full exclusive Semaphore to a Lock by ClassLoader+Classname. |
@struberg point is that the classloader does it and the transformer is called under that lock already edit: tested on tomee adding:
It works as expected:
In particular with the proposed patch ^^. Any way to patch liberty locally to run openjpa (creating my liberty.openjpa.jar with blueprint registration maybe?). edit 2: just checked openliberty sources (https://github.com/OpenLiberty/open-liberty/ right?), seems all classloaders are marked as concurrent but wrongly get the lock, in particular when there is a transformer so this is not an openjpa issue but an openliberty issue, no? Look https://github.com/OpenLiberty/open-liberty/blob/integration/dev/com.ibm.ws.classloading/src/com/ibm/ws/classloading/internal/AppClassLoader.java#L272 for ex, if no transformer is it correctly impl, if there is a transfomer it bypasses the lock :s. |
Created #65 as a proposed fix on master if you want to give it a try. |
Here's a version of the test bucket that will run with the jpa-2.2 complaint version of OpenJPA You'll need to edit the pom.xml file at the project root, and put in the full classname for the ProviderImpl class, as well as the directory where all of the jpa impl jar and its dependent libraries are (be sure to remove the jars containing javax.* packages) -- I moved the openjpa impl jar into lib so that all of the jars were in one dir:
|
I was able to reproduce the enhancement failure issue with OpenJPA 3.1.0 this way on Open Liberty. |
@jgrassel thanks a lot, I was able to test it on 3.1.2 too and also checked the proposed patch in my PR fixes that issue. |
Ok, glad to hear that it worked with 3.1.2 (the patch that is)! |
@jgrassel not with 3.1.2 (:() but with a patched 3.1.3-SNAPSHOT (still trying to find a good solution we both agree on before merging/pushing). Just meant I can reproduce the issue too with 3.1.2. |
OpenJPA's class transformer was designed in a time when ClassLoader access was synchronized. Because it was assumed that if the ClassLoader caused another class to load while it was already enhancing a class, causing another triggering of the class transformer, it was assumed that that could only happen if OpenJPA was bundled as part of the application (loaded by the app classloader in an EE environment), and was safe to assume that this reentrant invocation would not be a request to enhance a persistent type, and thus return null (no enhancement needed).
With Java 7+, ClassLoader locking has been changed (https://docs.oracle.com/javase/7/docs/technotes/guides/lang/cl-mt.html).
Now, because the same ClassLoader (and thus same transformer), can be invoked concurrently by different threads, this assumption was no longer valid.
OpenJPA needs to be updated to be mindful of reentrancy on a thread scope, instead of just simply reacting to reentrancy whenever it happens.
JIRA: https://issues.apache.org/jira/browse/OPENJPA-2817