Fix a race condition that leads to ClassAnalyzer with name CdiInjecteeSkippingClassAnalyzer exceptions#6049
Conversation
Happens when many threads execute REST client call at the same. Symptom: Occasional IllegalStateException: Could not find an implementation of ClassAnalyzer with name CdiInjecteeSkippingClassAnalyzer from WebTarget.request Reason for the exception: * The injectionManager is set earlier than it's used, it's stored to a shared volatile variable and later picked up * If some thread sets the shared volatile variable to another injectionManager, the original thread would use another threads injectionManager * In case the second thread doesn't bind the ClassAnalyzer class in time before the first thread uses it, the first thread won't have ClassAnalyzer bean available and throws exception
|
@OndroMih |
|
@jansupol , I wanted to set the threadlocal in each target instance initially. But I didn’t find a nice way to access all targets from The targets are not directly accessible from their top-level class, if I’m not wrong. They are referenced from a store, which only supports setting injection manager. I would have to adjust the store contract to somehow allow removing the thread locals from all targets. Do you have an idea how to do it? Is it worth it? |
|
@jansupol , maybe for a better performance, targets could set the thread local only if it's not set already. I read that ThreadLocal.get is faster than ThreadLocal.set, even if setting the same value that it already contains. Within a single invocation, the injection manager would always be the same so with the current change, the first target sets the ThreadLocal value and all others just set it again to the same value. If they call get instead, it could be faster. |
|
By trying that, I found out that the It looks like I struggle to find out a good way to remove the thread local, I don't know how to get the correct instance of |
|
I found the correct component that holds the thread local. It's the same extension which is used in the |
3ad72df to
d768a3c
Compare
Remove the thread local from the correct component (same as retrieved in the CdiComponentProvider.initialize method) Save a few nanoseconds by calling get instead of set on the thread local, log an error if a leaking injectionManager detected.
d768a3c to
dceaeaf
Compare
|
A tested detected a leak - in some cases, ClientRuntime initializes CdiComponentProvider again, after My solution is a bit hacky - move postInit for
The Another solution that also works would be to call Yet another solution would be to add a special method to The code in Or if I also add a default NoOp method to All of these solutions would work equally. The questions is which solution is better? What do you think, @jansupol , @senivam ? Is it better to treat the Or is it better to call |
111af26 to
faeee7a
Compare
If ClientRuntime needs CDI beans, it initializes CdiComponentProvider again, we need to clean up later.
faeee7a to
6189f01
Compare
|
@OndroMih Can this be replicated to Jersey 3.x, I replicated the issue using Glassfish 7.1.0 and I saw that is using Jersey version 3.1.11, also I tried with Glassfish 5 to test jersey version 2.x but in this case was not possible to replicate. Here part of the log: And do you think it is good to also include on Jersey 2.x? |
|
I think it’s OK for 3.1 branch. I’m not a Jersey committer but the Jersey team is active and some of my colleagues are in the team. Would you raise a PR for Jersey 3.1 branch with a port of the fix? if you’d like to port and release a fix also in the 2.x branch, ask committers from Oracle. OmniFish doesn’t maintain the 2.x but I saw some activity from Oracle on that branch, probably related to WebLogic. |
yes I will create the PR porting your fix to version 3.x |
|
I am sorry to report that this fix introduced a ClassLoader leak. |
|
I’m not sure how this can lead to the leak you described in the Payara issue. This change only impacts WebTarget - Jersey Client, not server. I don’t see any Jersey client usage in your reproducer. |
|
I am not sure either :) But the new code is clearly failing, the logs and the VisualVM screenshots prove it. |
|
Here are some stack traces, maybe it will shed some ideas?: |
|
Can you find out whether the |
|
Ondro,
It looks like non-static I do not have enough context for this fix to tell whats "good" and "bad" behavior here, |
|
So here is what I am observing:
In this stack trace, ThreadLocal gets set, and then Here is the In the below stack trace, the ThreadLocal gets set, but never removed, since |
|
@lprimak , are you sure you're running Jersey with this change? In your stacktrace, there's |
|
I am testing on Payara, they have their own patched version of Jersey, so the line numbers don't always match. |
Maybe just for total sanity, but can you try with the non-patched (regular) version? |
|
I tried running on Payara Micro with stock Jersey jars and can't reproduce the issue. The issue also doesn't happen in Embedded GlassFish 8.0.3. I can only see the issue in stock Payara Micro, which contains patched Jersey jars. See more details here: #6099 (comment) |
|
@arjantijms I ran un-patched 4.0.2 and the same issue exists. The errors printed are just the symptom, not the cause. They do not always appear. @breakponchito I would hold off working on the ports. This PR breaks all app servers, including GlassFish and Payara. |
|
Are you sure about that? I debugged Embedded GlassFish 8.0.3 and it doesn't execute the initialize method and setInjectionManager after the done method is called which removes the thread local. |
|
Yes, 100% Just to make it clear, this is the offending stack trace resulting in |
This solves the race condition - keep the effective injectionManager in a thread local. As multiple threads can modify the injectionManager and the
InjectionManagerInjectedCdiTargetinstances are global, effectiveInjectionManager must be scoped to the thread. It's actually scoped to a single WebTarget initialization and removed at the end of the initialization in thedone()method ofCdiComponentProviderThe problem:
A race condition happens when many threads execute REST client call at the same (more specifically, when
WebTarget.request()is called.Symptom:
Occasional
IllegalStateException: Could not find an implementation of ClassAnalyzer with name CdiInjecteeSkippingClassAnalyzerfromWebTarget.requestReason for the exception:
injectionManageris set in the initialization phase, before it's used. It's stored to a shared volatile variable and later picked upinjectionManager, before the original thread uses it, the original thread would use another thread'sinjectionManagerClassAnalyzerclass in time before the first thread uses it, the first thread won't haveClassAnalyzerbean available and throws exceptionA reproducer is described in payara/Payara#7753.
The original fix in Payara fork (53ae843) addresses this by a workaround - a retry in case of exception, which works, because by the next time, the other thread has enough time to bind the
ClassAnalyzerclass. But that solution isn't efficient.