Why does interruptible-eval from tools.nrepl queue evaluations?

93 views
Skip to first unread message

Carlo Zancanaro

unread,
May 2, 2018, 6:48:46 AM5/2/18
to Clojure
Hey there!

With tools.nrepl, if you eval two expressions they get queued up
and evaluated in sequence. This means that if I evaluate
(Thread/sleep 10000), and then immediately evaluate (+ 1 2), then
I have to wait ten seconds for the result of 3 to come back.

Is there a particular reason for this? Given that it's quite easy
to make it evaluate them in parallel, I figure there's a reason
why it was decided to evaluate them in sequence.

I have a use-case where I would like to be able to run evaluations
in parallel without having to wrap everything in (future ...), so
I'm considering writing some middleware to redefine
clojure.tools.nrepl.middleware.interruptible-eval/queue-eval to
just put things straight on the executor. It seems to work from my
limited tests, but are there any reasons why this would break
horribly?

Carlo
signature.asc

squeegee

unread,
May 2, 2018, 9:58:40 AM5/2/18
to Clojure
I suspect it's because queuing up operations is a relatively safe increment in convenience over the most naive implementation: having to wait for each evaluation to complete before submitting the next. 

Attempting to increasing the convenience further by evaluating expressions in parallel incurs the risk that a given evaluation might produce a side effect (e.g., a state change) that a later evaluation depends on.

--Steve 

Gary Fredericks

unread,
May 2, 2018, 8:07:33 PM5/2/18
to Clojure
I think dynamic vars in particular would be problematic. The repl is built around being able to set! certain vars, and you can't do that to the same binding from multiple threads.

Carlo Zancanaro

unread,
May 2, 2018, 9:36:26 PM5/2/18
to Gary Fredericks, Clojure


>I think dynamic vars in particular would be problematic. The repl is built around being able to set! certain vars, and you can't do that to the same binding from multiple threads.

The dynamic thread bindings are established within the function passed to queue-eval, though, so it seems like it will be as close to sensible as possible. (That is: use the bindings that were current at the point when the evaluation was started.)

I just looked into the interruption code, which is the only thing that looks like it actually depends on the serial nature of the evaluation. I think it would be possible to make it work with parallel evaluations, though, by storing a "message id to thread" map on the session instead of a single message id and thread. That would let you interrupt the thread for a given evaluation.

Carlo

Gary Fredericks

unread,
May 3, 2018, 6:38:25 AM5/3/18
to Carlo Zancanaro, Clojure
But what happens when a user set!s a var? is it effective for all future evals but not the concurrent ones?

Carlo Zancanaro

unread,
May 3, 2018, 9:02:20 AM5/3/18
to Gary Fredericks, Clojure
Apologies, I forgot to cc the list.

On Thu, May 03 2018, Gary Fredericks wrote:
> You would also have race conditions on the meaning of *1, *2,
> *3, and *e.
>
> On Thu, May 3, 2018 at 5:43 AM, Carlo Zancanaro
> <carloza...@gmail.com>
> wrote:
>> >But what happens when a user set!s a var? is it effective for
>> >all future evals but not the concurrent ones?
>>
>> I believe so, but I can't check right now. If I'm wrong I'll
>> correct myself when I can check.

It turns out I was half-wrong. When you start an evaluation then
the bindings will be set to the state that they were in at the end
of the most recent evaluation. This can get a bit confusing when
there are lots of overlapping evaluations, but when you're using
the system interactively (eg. with CIDER) then you have a lot of
control over the evaluation sequence.

The meanings of *1, *2, *3, and *e follow the same rules as the
above. I still find it a bit weird that evaluating a form with
C-M-x (or other in-buffer evaluation commands) changes those
variables, though. I would prefer it if those variables only got
set by an evaluation from the REPL buffer (which CIDER ensures are
sequential). That way I could eval stuff with C-M-x without
affecting the state of my REPL history variables. But that's a
separate issue in my mind.

Carlo
signature.asc

Gary Fredericks

unread,
May 3, 2018, 10:35:42 AM5/3/18
to Carlo Zancanaro, Clojure
Separately, I use this macro for running background things in the repl. It probably targets different concerns, but seems related at least.

Carlo Zancanaro

unread,
May 3, 2018, 5:32:05 PM5/3/18
to Gary Fredericks, Clojure

On Thu, May 03 2018, Gary Fredericks wrote:
> Separately, I use this macro
> <https://github.com/gfredericks/repl-utils#bg> for running
> background things in the repl. It probably targets different
> concerns, but seems related at least.

My use case is quite different. Requiring someone to decide ahead
of time to run code in the background (ie. by wrapping it in
another form) won't work for me at all.

I'm trying to write a Common Lisp interactive restart system,
somewhat similar to what Slime gives you. It actually works pretty
well, but it's often helpful to be able to redefine things, or to
run some code to change a data structure, before selecting a
restart. While waiting for a restart selection the eval thread is
blocked, though, so other evaluations have to happen in another
thread. The nrepl protocol seems like it was designed for this,
given how asynchronous it is, so I was surprised that
interruptible-eval explicitly queued up evaluations and ran them
sequentially.

I've ended up doing this as a solution:
https://github.com/czan/dont-give-up.nrepl/blob/acc20e0c25aa90a5c991b9cd1f7cc4abe2f1cd6b/src/dont_give_up/nrepl.clj#L335.
This isn't perfect. There are further modifications required to be
able to interrupt evaluations other than the most recent one, but
it works well enough to be useful. If anyone has a reason why this
is a terrible idea, I'm all ears.

Carlo
signature.asc

Gary Fredericks

unread,
May 3, 2018, 5:37:24 PM5/3/18
to Carlo Zancanaro, Clojure
I used the hacky tactic of just sending messages through the nrepl channels (while the eval is blocked) to implement this: https://github.com/gfredericks/debug-repl

Bozhidar Batsov

unread,
May 4, 2018, 6:54:50 PM5/4/18
to clo...@googlegroups.com
By channel you mean sessions right? That's a simple way to circumvent the serialized nature of evaluations - you can just spin a new session for each evaluation. Never benchmarked what's the overhead of this, though. 

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+unsubscribe@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages