-
-
Notifications
You must be signed in to change notification settings - Fork 53
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
Modifications to classpath by middleware are not visible to user nREPL session #103
Comments
Having thought about it for a short bit, I think option 1 above better achieves the principle of least astonishment. I think any middleware author would expect the user's session to reflect any explicit classpath changes made by the middleware. I know I did. Understanding the current behavior was a brain teaser. |
Great investigation, @jeffvalk! Thanks for tracking down the root cause of this. I guess this has been broken ever since the update to dynapath 1.0. I also prefer option 1). |
Hi, I'd like to contribute a question/observation: to what extent is runtime classpath modification a core need of Orchard? I say this because, like other Clojure developers, I feel a bit wary of fiddling much with classloaders. For example I contributed https://github.com/clojure-emacs/cider-nrepl/pull/668 - a POC showing that the mere presence of CIDER can affect other code. One can also make the observation that neither Lein or deps.edn officially provide runtime classpath modification. clj-refactor also disabled it at some point. If the extent of this runtime classpath modification is "provide sources and javadocs", one can consider alternatives. I have completed this Lein plugin which accomplishes so clojure-emacs/cider-nrepl#64 (comment) using traditional means - the classpath is calculated beforehand instead of mutated. If there are no other use cases, it might be worth considering if Orchard's internals can be simplified. This can have other desirable effects, e.g. newcomers can understand better its execution model (since not everyone has expertise in JDK or classloader intricacies). |
Personally, I'd say the current use case is something any development environment would want. In-editor documentation and source navigation for JDK classes seem like core requirements to me. As best I can tell, the alternative to the current approach would be adding the Regarding using a lein plugin to replace this specific use case:
Regarding avoiding classpath dynamism:
|
Thanks for the reply!
A Lein plugin can do arbitrary computation, so the existing logic for finding There's the precedent of https://github.com/pallet/lein-jdk-tools which does a similar job (although more basic: it doesn't download any files since often they are already included when installing the JDK).
Yes. The plugin I linked to was written just a couple weeks ago. It's a vanilla Clojure program (it doesn't need
The language may be dynamic, but the platform can have some constraints...
Agreed. I'd humbly suggest to make sure we're not repeating errors:
In constrast, Lein's approach of doing classpath calculations "ahead-of-time" seems very much time-proven, well-aligned with JVM's assumptions and compatible with other plugins like deps.edn. |
I'll just add here that the only reason why this got disabled is that Java 9 broke it. The users loved this functionality and I think it was quite useful.
Can't agree more!
I agree this approach works fine the use case with sources, although knowing how often users are asking for such functionality, not to mention this is at the heart of nREPL's sideloading, which obviously can't be achieved otherwise. |
I'll also add that it seems to me our real issue is that lack of time - we're not really facing any insurmountable issues, but there's a lot of work to go around and no one has much time to dedicate to it. That's why breaking changes often go unaddressed for years (e.g. the ClojureScript completion has also been broken for a while, after some changes were done to shadow-cljs and the maintainer of compliment didn't have time to address those). I myself, try to focus more on the Elisp code, as there are fewer people willing to touch it, compared to those that might fix something occassionallly in |
Yes and yes. This is core Java platform functionality. Application servers and all manner of "enterprise" Java depend on this design. And every time we load a clojure file or evaluate a statement at the REPL, we use it. |
Finally tracked this one down. When the nREPL session middleware creates the session classloader, it passes the context classloader as the parent here and here. At the time middleware is loaded, the context classloader is the application classloader, which is not modifiable. For the session to see classpath modifications made when middleware is loaded, two things must be true:
(clojure.lang.DynamicClassLoader.
(.getContextClassLoader (Thread/currentThread))) ; <- jdk.internal.loader.ClassLoaders$AppClassLoader to something like this: (clojure.lang.DynamicClassLoader.
@clojure.lang.Compiler/LOADER) ; <- clojure.lang.DynamicClassLoader when creating the nREPL session. I say "something like" because in practice, I'd suggest we use the highest modifiable parent in the compiler's hierarchy, not the lowest. |
Before making any changes, it probably makes sense to ask again whether this is the best way to address this. An alternative would be to make modifications to the classpath within the session. Would this better reflect what a session is designed to do? The tradeoff is that this would create a specific dependency on the session middleware, and the session object would have to be passed as an argument to various orchard functions. |
As Orchard should be usable with other REPLs (e.g. a socket REPL) we can't really exponse any session dependencies in it.
The best approach would be one that keeps this functionality reusable between REPLs. If that's not feasible, I guess we'll have to move this fully to nREPL - e.g. we can expose some op to add resources to the classpath. |
That certainly makes sense. I pushed a change that addresses this in Orchard without any regard to what the underlying REPL does. It verifies the classpath every time the added resources might be referenced. If the context classloader doesn't share a modifiable ancestor with the previously seen classloader(s), the classpath is updated. It may be a bit brute force in approach, but I think that's a required tradeoff for REPL portability. Regardless, it's simple and works. |
The change looks good to me, I was just wondering about 3rd party Java library sources. Do we need to change something for them to work if they are present on the classpath?
Btw, regarding this alternative approach - do we really need the session itself or just it's classloader? I guess it's fine to add explicit/optional classloader params to functions that might benefit from one. |
No, everything else is the same. This commit doesn't affect the static classpath at all, and shouldn't impact any other dynamic classpath modifications (e.g. from other middleware) either.
We'd only need its classloader. |
See clojure-emacs/orchard#103 for details.
This fixes #2732. It also fixes #2687. The actual fix is in orchard/cider-nrepl. See clojure-emacs/orchard#103 for more details.
I've cut new Orchard and cider-nrepl releases including the fix. It will soon trickle to CIDER's |
Is there a concrete example of something that didn't work before that works now? I'm not sure if I understand fully what this newly allows / fixes. I tried going to definition on |
That's exactly the type of problem that this is supposed to fix. Basically before the fix we'd add the JDK sources to the classpath dynamically, but they weren't in the classloader hierarchy of the REPL sessions and this results in the JDK sources not being available when needed. You do have the JDK sources installed, right? |
Not sure... how would I do that? I've installed OpenJDK. If I run
Does installing the JDK not install the source? If that's the case, then I guess that means there's a little bit of extra work for a dev to get these features? I assume most people would just install the JDK. |
On Linux the sources are a separate package (at least on Debian-like distros), I'm not sure how it is on other OS-es. You should have a file named |
@jeffvalk Btw, how did you test that this change works? I just got to playing with this myself in CIDER and navigation to code and docs is still broken. E.g. this is what I get for
|
Prior to e3de281, I'd only tested launching via I just pushed 0768f8e that works with both. I tested it launching both with |
@jeffvalk It seems that a lot of people are running into |
I'm surprised to see this is still an issue -- I cannot work in Cider with Java 8, even though that's a perfectly valid deployment platform. Is there anything I can do to help? I have completely changed laptops since first experiencing this, and reinstalled absolutely everything, and I still can only use Cider with Java 11 (Or, I guess I can pin myself to an earlier version.) |
I'm sorry about the problems this update has caused. I was hoping that @jeffvalk would find time for this, but I guess he has been busy. I won't be able to dive into this any time soon, but I can revert the changes if no one has better ideas. |
I (ref: earlier in this thread) was long thinking about proposing making all the Earlier this week I actually tracked down the root cause of clojure-emacs/cider-nrepl#668 to dynapath in orchard. So there are at least two completely different problems caused by this dependency/implementation. @bbatsov: would you appreciate if I created a proposal / problem exposition in a separate GH issue? I think things are pretty self-evident by now, but I can also do some writeup. |
@vemv yeah, that'd be great. I agree it'd be best to remove dynapath from Orchard and potentially move this responsibility to the Orchard clients. For me it makes sense for them to be doing such classpath modifications if necessary. |
Closes clojure-emacs#112 Related clojure-emacs#103 Related clojure-emacs#105
Closes clojure-emacs#112 Related clojure-emacs#103 Related clojure-emacs#105
Closes clojure-emacs#112 Related clojure-emacs#103 Related clojure-emacs#105
Closes clojure-emacs#112 Related clojure-emacs#103 Related clojure-emacs#105
Closes clojure-emacs#112 Related clojure-emacs#103 Related clojure-emacs#105
Closes clojure-emacs#112 Related clojure-emacs#103 Related clojure-emacs#105
Useful resource for people who want to learn more about classloaders and Clojure https://lambdaisland.com/blog/2021-08-25-classpath-is-a-lie |
For people tracking this issue because of errors such as For the underlying problem ( |
Summary
Middleware is loaded in a different classloader context than user interactions with that middleware. The latter shares no modifiable classloader parent with the former, so changes made to the classpath when middleware is loaded are not seen by user interactions.
Impact
The primary example of this is in
java.clj
, which adds the JDKsrc.zip
to the classpath and sets thejdk-sources
variable to the zip file's path if found. In-editor JDK class/method docstrings (and argument names, etc) depends on this feature, as does source navigation (jumping) for JDK classes.The following issues appear to describe this bug:
Scope
This behavior happens on both JDK8 and 9+.
dynapath
formerly hackedjava.net.URLClassLoader
to allow it to be modifiable on JDK8 and below, which would have prevented this issue on JDK8 (albeit in an unsupported way), but recent versions ofdynapath
removed this hack, so all JDK versions behave the same wrt this issue.Investigation
Below are the values of
jdk-sources
, the classpath, and the classloader hierarchy at middleware load time (via print statements in thejava
middleware), and at runtime in a user REPL session. Note that:src.zip
has been found (jdk-sources
is set); howeversrc.zip
is visible on the classpath at load time, but not at runtime in the REPL session, becauseDynamicClassLoader
in their respective classloader hierarchies. The lowest common ancestor of the context classloader for these contexts is anAppClassLoader
, which is not modifiable. Hence, any modification to the highest modifiable classloader in the first context is not visible in the second.At load time:
And at runtime, via the REPL:
Analysis
This may be as much an nREPL discussion as an orchard one. I suspect it may have to do with
*bindings*
(including theclojure.lang.Compiler/LOADER
) being stored and passed as the:session
for nREPL messages -- essentially that these contexts are intentionally separated.If so, testing for correct behavior requires both contexts. It can't be tested purely within a CIDER/nREPL session (as one would do in development), nor would our current approach to unit tests catch any breakage.
At a high level, there seem to be two ways to make this behave as intended:
@bbatsov What do you think of this?
The text was updated successfully, but these errors were encountered: