Skip to content

Commit 510a7b5

Browse files
authored
ensure nREPL server can exit even when client connections are open (#1003)
Hi, can you please review patch to let nREPL server to exit even when client connections are still open. It addresses #1002. This is simply done by setting the server's `daemon-threads` field to true. Additionally, I've updated the `:server*` option accepted by `server-make!` from an `atom` to a `promise`. This change aligns with the original intent of providing a server reference when it becomes available. Note that this is a breaking change as it alters the API. Given the current development stage and no known users of this API, I believe it's safe to include this change now. Let me know if you'd prefer this in a separate PR or think otherwise. I've added a test for the same. Thanks Co-authored-by: ikappaki <[email protected]>
1 parent 4f5e6c6 commit 510a7b5

File tree

3 files changed

+47
-22
lines changed

3 files changed

+47
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
* Improved on the nREPL server exception messages by matching that of the REPL user-friendly format (#968)
1818
* Types created via `deftype` and `reify` may declare supertypes as abstract (taking precedence over true `abc.ABC` types) and specify their member list using `^:abstract-members` metadata (#942)
1919
* Load functions (`load`, `load-file`, `load-reader`, etc.) now return the value of the last form evaluated. (#984)
20+
* nREPL server no longer hangs waiting for client connections to close on exit (#1002)
2021

2122
### Fixed
2223
* Fix inconsistent behavior with `basilisp.core/with` when the `body` contains more than one form (#981)

src/basilisp/contrib/nrepl_server.lpy

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -433,8 +433,10 @@
433433
(let [{:keys [host port] :or {host "127.0.0.1" port 0}} opts
434434
handler (python/type (name (gensym "nREPLTCPHandler"))
435435
#py (socketserver/StreamRequestHandler)
436-
#py {"handle" #(on-connect % opts)})]
437-
(socketserver/ThreadingTCPServer (python/tuple [host port]) handler)))
436+
#py {"handle" #(on-connect % opts)})
437+
server (socketserver/ThreadingTCPServer (python/tuple [host port]) handler)]
438+
(set! (.-daemon-threads server) true)
439+
server))
438440

439441
(def ^:private nrepl-server-signature
440442
"The de facto signature nrepl started message that is used by IDEs to identify the
@@ -455,8 +457,8 @@
455457
to pickup a random available port.
456458
:keyword ``:nrepl-port-file``: The file to write the port number to, it defaults
457459
to .nrepl-port.
458-
:keyword ``:server*``: An atom to hold the server reference as returned from
459-
:lpy:fn:`server-make` , useful for testing.
460+
:keyword ``:server*``: A promise object that will receive the server reference
461+
from :lpy:fn:`server-make` when is made avaiable.
460462

461463
.. seealso::
462464

@@ -467,11 +469,11 @@
467469
(let [{:keys [nrepl-port-file server*]
468470
:or {nrepl-port-file ".nrepl-port"}} opts
469471
server (server-make opts)]
470-
(when server* (reset! server* server))
471472
(try
472473
(let [[host port] (py->lisp (.-server-address server))]
473474
(println (format nrepl-server-signature port host host port))
474475
(spit nrepl-port-file (str port)))
476+
(when server* (deliver server* server))
475477
(.serve-forever server)
476478
(catch python/KeyboardInterrupt _e
477479
(println "Exiting in response to a keyboard interrupt..."))

tests/basilisp/contrib/nrepl_server_test.lpy

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -605,26 +605,48 @@
605605
(throw python/ZeroDivisionError)))))
606606

607607
(testing "nrepl-server port file and address"
608-
(let [server* (atom nil)
608+
(let [server* (promise)
609609
[fd filename] (tempfile/mkstemp "nrepl-server-port-test")]
610610
(doto (threading/Thread
611611
**
612612
:target #(nr/start-server! {:server* server* :nrepl-port-file filename :host "0.0.0.0"})
613613
:daemon true)
614614
(.start))
615-
(try
616-
(time/sleep 1) ;; give some time to the server to settle down
617-
(is @server*)
618-
(is (bio/exists? filename))
619-
(let [port-filename (slurp filename)
620-
server @server*
621-
[host port] (py->lisp (.-server-address server))]
622-
(is (= host "0.0.0.0"))
623-
(is (= (str port) port-filename)))
624-
625-
(finally
626-
(doto @server*
627-
(.shutdown)
628-
(.server-close))
629-
(os/close fd)
630-
(os/unlink filename))))))
615+
(let [server (deref server* 1 nil)]
616+
(is server)
617+
(try
618+
(is (bio/exists? filename))
619+
(let [port-filename (slurp filename)
620+
[host port] (py->lisp (.-server-address server))]
621+
(is (= host "0.0.0.0"))
622+
(is (= (str port) port-filename)))
623+
624+
(finally
625+
(doto server
626+
(.shutdown)
627+
(.server-close))
628+
(os/close fd)
629+
(os/unlink filename)))))))
630+
631+
(deftest nrepl-server-shutdown-test
632+
(testing "server can exit even when client connections are still open."
633+
(let [server* (promise)
634+
server-thread (threading/Thread
635+
**
636+
:target #(nr/start-server! {:server* server*})
637+
:daemon true)]
638+
(.start server-thread)
639+
(let [server (deref server* 1 nil)]
640+
(is server)
641+
(let [[host_ port] (py->lisp (.-server-address server))]
642+
(binding [*nrepl-port* port]
643+
(with-connect client
644+
(client-send! client {:id 1 :op "clone"})
645+
(let [{:keys [status]} (client-recv! client)]
646+
(is (= ["done"] status)))
647+
;; Shutdown server during an active client connection
648+
(let [shutdown-thread (future (doto server
649+
(.server-close))
650+
:done)
651+
status (deref shutdown-thread 1 :time-out)]
652+
(is (= :done status))))))))))

0 commit comments

Comments
 (0)