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
Dynamic loading of middleware #185
Conversation
Yeah, that's one idea I like very much. One big issue with the deferred loading approach taking currently by |
I'm impressed with the simplicity of the solution! Great work! I'm also tempted to add a ping middleware to nREPL, as a simple example for a middleware. :D Reminds me of SLIME and SWANK. :D It'd be nice if you incorporated more of your descriptions from the ticket in the middleware's implementation as docstrings and comments. |
Thanks for the comments! Made some quick changes in response. I take it you are, in principle, supportive of the design? I'd like to do a bit more testing of the core capabilities, while I finish off the PR. More comments from the @nrepl team welcome |
Just hit a good milestone:
A few new realisations/discoveries:
Will ponder more about this, but quite glad to see this work mostly as envisioned. |
@@ -60,3 +60,20 @@ | |||
(require (symbol (namespace sym))) | |||
(resolve sym) | |||
(catch Exception _)))) | |||
|
|||
(defmacro with-session-classloader |
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.
You'll have to make sure that hotloading still works. I'm always afraid we might break it when fiddling with the classloader, as it doesn't really have any tests.
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.
hotloading as in what Pomegranate does?
Dynamic loader capabilities is fairly separate from sideloading, if that's what you are concerned about. This macro is from the changes made in #162, which has its origins in unrepl. Both sideloading + dynamic-loader while sideloading are covered in tests.
I was looking at #113 : is that the kind of breakage you are concerned about? Maybe the the thing to do is to add more tests that we are concerned about?
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.
hotloading as in what Pomegranate does?
Yep. It didn't work with nREPL until we did some classloader modifications a while ago, so I'm always wary of us breaking this again.
I was looking at #113 : is that the kind of breakage you are concerned about? Maybe the the thing to do is to add more tests that we are concerned about?
Yeah, we should probably tackle this at some point. Initially we skipped the tests because of the many moving pieces there (hotloading library, build tool).
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.
I've made the macro a bit safer. If there's no :classloader
under (meta session), then it's pretty much a do
block.
Currently, only the sideloader sets :classloader
, so it's quite unlikely to make any breaking changes. I'd be happy to chat more about tests/classloader etc. separately though?
Looks solid to me! Apart from my small remarks, I think the biggest thing missing is more documentation for client authors. |
@shen-tian I think we're good to merge. Ideally you should clean up the commit history a bit by squashing related commits together (e.g. - it seems the first 9-10 commits can be reduced to one). |
Ah, actually you'll also have to run |
Will do. Was assuming you'll squish :) |
From the web UI I can only squash all commits in 1. I think it'd better to do manual squashing in this case, as there 3-4 meaningful changes apart from the dynamic loader itself. |
c529144
to
ce0cd2a
Compare
@bbatsov Squished it down to 6 commits, including regen of the ops page. |
Included adding tests both for the middleware, but also an integration test for using it with the sideloader.
Updating docs, including expanding the building clients section. - Deprecating the default-middlewares var. - Make with-session-classloader even safer - Revert ns indentation for describe-test - Add server/built-in-ops, remove uses of default-middlewares - Expanding on "building clients" and other doc changes - Update auto-generated docs on built-in ops
🎉 |
Pending resources in the sideloader are kept in an atom, which currently is defined in a closure which closes over the classloader and sideloader ops. When the middleware stack is rebuilt (e.g. via an `add-middleware` op), this atom gets re-initialized, and the state is lost, causing the side loader to respond to previously requested resources with `:unexpected-provide`. The only state that does persist is the session. We already use the session to store the classloader itself, it makes sense to also keep the `pending` atom in the session, so that the pending state that we see always corresponds with the current session classloader. This fixes an issue which was pointed out in the PR that adds dynamic middleware loading: nrepl#185 (comment)
Pending resources in the sideloader are kept in an atom, which currently is defined in a closure which closes over the classloader and sideloader ops. When the middleware stack is rebuilt (e.g. via an `add-middleware` op), this atom gets re-initialized, and the state is lost, causing the side loader to respond to previously requested resources with `:unexpected-provide`. The only state that does persist is the session. We already use the session to store the classloader itself, it makes sense to also keep the `pending` atom in the session, so that the pending state that we see always corresponds with the current session classloader. This fixes an issue which was pointed out in the PR that adds dynamic middleware loading: #185 (comment)
This add a new middleware,
dynamic-loader
(open to other suggestions), that allows us to modify the middleware stack on the fly. When combined with sideloading, this would allow us to inject middleware that the host JVM is not even aware of at startup.A possible use case, e.g. CIDER, to connect to an already running nREPL instance, check what middleware are loaded, and if missing
cider-nrepl
, load it.Closes #143
API
The new middleware defines three new ops:
ls-middleware
: returns a list of middleware currently loaded.add-middleware
: add a collection of middleware to the existing stack. This will re-sort all the existing middleware as well as the new ones.swap-middleware
: replaces the existing stack with a new set of middleware.Implementation
This is done via a new middleware,
dnyamic-loader/wrap-dynamic-loader
. There are two interesting bitsPassing of state
An earlier design had the
dynamic-loader
being a specialised handler, and not a middleware. It was initialised with a list of middleware. This worked quite well, and the handler held its own state of what middleware are active. However it had to be the top most layer, so we can't use any upstream middleware in this handler. In particular, not having access tosession
made sideloading difficult.Thus, the current design used an external state atom that stores
:handler
, the active handler fn built out of all the middleware and:stack
, the list of individual active middleware. Inserver/default-handler
, we create this atom, and close over it to return the handler.We the bind a
*state*
var in thedynamic-loader
ns to this atom when calling the handler. This allows the middleware to modify the global(ish) handler/stack state. (The state is not actually global. It will be created each time we create a new server viadefault-handler
).This allows us to preserve
wrap-dynamic-handler
as a 1-arity fn that works well withset-descriptor!
andlinearize-middleware-stack
etc., while effectively passing the state in.Here's the key lines of code, showing how we create state, bootstrap the stack, and close over the state in the resultant handler:
Open to suggestions on this approach. It's a bit loopy, but the weirdness is concentrated in very few places, and makes it easy to reason able this middleware like any other, most of the time.
Working with the sideloader
When updating middleware, this will look to the same
(:classloader (meta session))
value that the sideloader uses, just likeinterruptable-eval
. You'll want to enable the sideloader in the "nREPL config" session, update the middleware, and be free to use the middleware without having to deal the sideloader during regular use.Note that the
dynamic-loader
doesn't interact directly withsideloader
, thus can be before or after it on the stack. However, it does requiresession
to come before it, just for the sideloading case.Other notes