-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Cache Class
instances that come from the library itself
#987
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: master
Are you sure you want to change the base?
Conversation
Class loading and lookups can be costly in some contexts. For example, Tomcat can in some cases throw `ClassNotFoundException`s internally while looking up a class name, even if it has already been loaded. By caching `Class` instances for the classes coming from the library itself, we can avoid these exceptions (except once per class). Inspiration taken from the `Services` cache.
Thanks @slovdahl ! So you could end up a condition like:
I think at one point I had messed around with a more complex cache key. Any thoughts on this? |
Because of Classloaders, and their very different runtime contexts (web apps, native apps, OSGi, etc) we can't safely cache based on name alone, which is the purpose of the current logic that cascades in order of likelihood priority:
Since (almost all?) name-based instantiation code in JJWT internals does not specify a classloader (on purpose, to allow this dynamic classloader technique), the existing loading logic is the only way to perform safe lookup. The other way is to use the Classloader as part of the cache key, but then we'd have to refactor all the code in the project to specify classloader at lookup time, which isn't feasible at all. Additionally, it's not easily possible to know which classloader should be specified at lookup time, since, if it works on the first classloader within Tomcat, but it fails internally in Tomcat again, and JJWT falls back to another classloader, the two resulting classes would be different, and probably fail instantiation since the one needed (that previously worked before) wouldn't be the one found by Tomcat in that different context. The current 3-classloader-cascade technique is used by most modern framework libraries that perform identical behavior - Spring, JBoss, etc. There's just no easy way around it if using name-only lookup. Also, it's not clear to me how much of a problem this is in practice. Wouldn't any This also concerns me:
Without knowing more about why this happens in Tomcat, shouldn't this issue be reported so that Tomcat can fix it and not impose a burden on downstream libraries? If a class has been successfully resolved previously, why would Tomcat have this (non deterministic?) behavior? Very confusing. All of this said, I would be extremely hesitant to change this logic since it is the backbone of api-to-impl loading that has been working for the life of the project, and this type of concern hasn't been reported before. I'm not saying it's a perfect solution, but I am saying that the current logic seems to be fine for 99.99% of JJWT use cases. And given its breadth of impact, changing it could cause huge problems for the extended JJWT user/application base, so I'd rather not. Perhaps another angle - if this Tomcat behavior is deterministic, i.e. we understand the context of how these things occur - perhaps the order of classloaders attempted can be configurable to avoid the I'd much rather work towards a deterministic solution than changing the current logic which is supposed to be a "things aren't deterministic, so this is the best/safest fallback" approach and works well for that purpose. Thoughts? |
Thanks for the quick feedback @bdemers and @lhazlewood! I agree that this suggestion might not always work in case class loaders come and go, thanks for pointing that out. Usually though, class loaders delegate to their parent if a class is not found, so as long as class loaders are hierarchical, the same
Yep 👍 One thing that I believe is different though (but please correct me if I'm wrong): it's not that common to do explicit class loading on paths that are potentially hot. My initial reaction when I noticed that JJWT does explicit class loading when calling e.g. (That said, I understand why it's done the way it is. Separating the API and implementation is really nice.)
I probably should have made this part more clear. The exception that Tomcat throws internally is also caught by Tomcat itself, so no fallback takes place in JJWT. So in that sense Tomcat's behaviour is deterministic. When I step through the code on our side it ends up calling There's actually another performance-related problem too that I didn't notice before. Tomcat also takes a lock right away in the
A bit surprising indeed, but maybe there's something in our way of using that makes it stand out. This popped up in profiles when stress testing our application, so that's why I ended up here. Wild idea, and I realize it would be a huge rewrite, but could the |
And thanks for the thoughtful discussion! I appreciate the work you've put into the discovery around this issue - it's a level of depth that we can't easily see, thank you 🙏 .
You'd be surprised what we've seen with OSGi and things like dynamic webapp loading/unloading 😅 .
I don't know how common it is, but we have received issues related to dynamic classloader environments, so we're trying to ensure things 'just work' in all environments.
Yep! That's indeed the reason for the design.
This I think is the core of the issue, and regardless of how we might address things in JJWT (more on that below), it seems like it should be addressed with the Tomcat team. As most of us are aware, using exceptions for control flow is frowned upon, yet it is something being done in this particular part of Tomcat source code. Further, the lock they acquire preemptively is the It seems like their implementation should be improved to alleviate this problem, especially since it's all hidden from JJWT, or any other webapp or library that may use reflection after application start-up. For example, how many Spring apps experience this same issue since they can also use heavy reflection-based new instance creation (i.e. anything marked as Spring 'prototype' scope). JJWT would be just a tiny blip on the radar compared to that.
I very much appreciate your stress testing efforts! That's always tedious (but valuable) work - thanks for reporting things 😄.
I thought about this quite a bit this morning to see if I could find alternatives. The
But thinking through this more, and doing a 'find usages' for the
After seeing this, I wondered if we can do the following:
I think this would work because the factory instances would be cached in the api classes, and even if classloaders are replaced in a dynamic context, new static singleton factory instances would be created, and used by the code/context using the Would you want to refactor your PR to try this technique? Or if it feels a little daunting, I'd be happy to do it as long as you're able to test the new code/artifacts in your test suite since I couldn't re-create that easily. Thoughts? |
P.S. I forgot to mention: While So we created the As such, using the internal JJWT |
Thanks for idea! I'll need some time to digest your suggestion while juggling other priorities, but at the face of it it sounds like a great idea. I'll give it a try and ask for help if needed. |
Class loading and lookups can be costly in some contexts. For example, Tomcat can in some cases throw
ClassNotFoundException
s internally while looking up a class name, even if it has already been loaded. By cachingClass
instances for the classes coming from the library itself, we can avoid these exceptions (except once per class).Inspiration taken from the
Services
cache.