Fork me on GitHub
#nrepl
<
2024-05-08
>
Hasnain Naeem01:05:04

What's best way to achieve isolation between clients in nREPL? For example, if we have several clients connected to same server and each one can see variables and functions defined by others. But, aren't sessions supposed to provide some level of isolation?

phronmophobic02:05:51

It depends on what you mean by isolation, but sessions probably don't provide the kind of isolation you're looking for.

phronmophobic02:05:03

What kind of isolation are you looking for? What's your use case?

Hasnain Naeem02:05:44

@U7RJTCH6J thank you for your response. The use case requires each session to be a clean slate and other sessions shouldn't be able to access whatever variables/functions are defined inside it. I can enforce usage of a fixed namespace for each session and limit access to others to achieve this. But, I was curious what kinda isolation nREPL already offers. I guess it doesn't offer any at all.

Hasnain Naeem02:05:08

Does nREPL offer any isolation at all?

phronmophobic02:05:58

I'm not aware of that kind of isolation being provided by nrepl. I think it's primary goal is just evaling forms. You would need some other mechanism to provide isolation. I think that could theoretically be done via middleware, but any architecture that wants to provide isolation between multiple clients running arbitrary code in the same process seems fairly dubious.

Hasnain Naeem02:05:59

Documentation states: > Sessions persist dynamic vars (collected by get-thread-bindings) against a unique lookup. This allows you to have a different value for *e from different REPL clients (e.g. two separate REPL-y instances). I tried following:

(defn concurrent-interactions-with-same-namespaces-using-dynamic-vars []
  (with-open [conn (repl/connect :port 7888)]
    (try
      (let [client1 (repl/client conn 1000)
            clone-response1 (repl/message client1 {:op "clone"})
            session-id1 (:new-session (first clone-response1))

            client2 (repl/client conn 1000)
            clone-response2 (repl/message client2 {:op "clone"})
            session-id2 (:new-session (first clone-response2))

            op-response1 (first (repl/message client1 {:op "eval"  :code "(def ^:dynamic *todo-file* \".session1todo\")" :session session-id1}))
            op-response2 (first (repl/message client1 {:op "eval"  :code "*todo-file*" :session session-id1}))
            op-response3 (first (repl/message client2 {:op "eval"  :code "*todo-file*" :session session-id2}))]

        ;; clone
        (println "\nClone Response1 from nREPL: " clone-response1)
        (println "\nClone Response2 from nREPL: " clone-response2)

        ;; eval
        (println "\nResult2 from nREPL: " op-response1)
        (println "\nResult4 from nREPL: " op-response2)
        (println "\nResult5 from nREPL: " op-response3)
      (catch Exception e
        (println "An error occurred: " e)))))
Dynamic variable defined for session 1 is accessible in session 2.

phronmophobic02:05:00

Yes, you could have two separate repls in the same environment.

phronmophobic02:05:13

That's not the same thing as two separate repls in separate environments.

phronmophobic02:05:23

Is the isolation for security or just so sessions don't clobber each other?

Hasnain Naeem02:05:18

To prevent clobbering

phronmophobic02:05:31

ah ok. that makes sense. You can do that. def alters the var's root. You would need the middleware to use binding to create thread local values for vars for them to be separate in separate sessions.

Hasnain Naeem02:05:46

I see. Thank you. Documentation says: > Sessions persist dynamic vars (collected by get-thread-bindings) against a unique lookup. What exactly is this about if not about allowing isolation of dynamic variables for various sessions.

phronmophobic02:05:20

I think that is allowing isolation of dynamic variables.

phronmophobic02:05:52

However, you have to create thread locals for the dynamic variables. Updating the var root is a global operation (eg. def).

👍 1
phronmophobic02:05:49

I'm trying to find a relevant example, but most repls will setup thread locals for common dynamic variables.

Michael Griffiths16:05:44

Clojure namespaces and vars are generally https://clojure.org/reference/vars. Only vars declared dynamic (and hence returned by clojure.core/get-thread-bindings) can be set! – if this is done in an nREPL session it will be invisible to other sessions

thanks3 1
Michael Griffiths16:05:04

Some isolation is possible by manipulating JVM classloaders – examples that spring to mind are classlojure, clj-embed and clojail. It’s fraught with complexity though and Clojure isn’t designed to support this so you can run into problems if you need to share anything between the runtimes

Michael Griffiths16:05:28

I don’t know all the details of your use case, but if process isolation is an option then it’s very possibly less painful than the classloader route

👍 1