babashka 2025-05-08

hi friends, I was trying out httpkit server, and run-server was not halting the app to listen for connections - I found another sample in the wild where someone ends their .bb file with:

@(promise)
why is this necessary? it's a promise that will never resolve/deref right? in plain old clojure though, I don't think such a thing is necessary for httpkit (is it?)

bb seems more aggressive than clojure when shutting down. compare bb -e '(future @(promise))' and clojure -e '(future @(promise))'.

👍 1
🙏 1

clojure will wait a bit before shutting down, you need to call shutdown-agents manually but bb does this for you clojure -X or -T (forgot which) has altered this behavior for clojure as well since it just makes more sense for scripts/tools

🙏 1

but bb simply just calls shutdown-agents at the end of the script

Perhaps I could try to do the daemon thread approach in bb, but so far this worked out ok

👌 1

I double checked an httpkit server example invoked with -X and it doesn't seem to quit right away:

(ns script)

(require '[org.httpkit.server :refer [run-server]])

(defn handler [_req]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello from http-kit in Babashka!"})

(defn doit [_]
  (run-server handler {:port 8080})
  (println "Server running at "))

;; @(promise) ; Keeps the server running
$ clj -X script/doit
Server running at 
Although httpkit does seem to spawn daemon threads. Not sure why it doesn't quit right away with -X here

Yeah, this is the difference:

(ns script)

(defn doit [_]
  (-> (doto (new java.lang.Thread (fn []
                                    (Thread/sleep 1000000)) )
        (.setDaemon true)
        (.start)))
  (println "Server running at "))
So if you remove .setDaemon the script will immediately return with clojure -X

Oh no, wait, it's not about shutdown-agents, it's about bb doing a System/exit :)

(defn doit [_]
  (-> (doto (new java.lang.Thread (fn []
                                    (Thread/sleep 1000000)) )
        #_(.setDaemon true)
        (.start)))
  (println "Server running at ")
  (System/exit 0))
This is basically what's happening

I guess if I would change babashka.main to:

(let [exit-code (run args)]
      (when-not (zero? exit-code)
        (System/exit exit-code)))
+ installing the daemon executor, it would resemble clojure closer

lol:

lein test babashka.agent-test
=== agent-binding-conveyance-test


Too long with no output (exceeded 10m0s): context deadline exceeded

😱 1

locally this test does just terminate though:

$ ./bb -e '(def ^:dynamic *foo* 1) (def a (agent nil)) (binding [*foo* 2] (send-off a (fn [_] *foo*))) (await a) @a'
2

With the change in place, this does still work but hangs:

(import '(java.util.concurrent Executors ExecutorService))
(let [fut (.submit ^ExecutorService (Executors/newCachedThreadPool) ^Callable (fn [] 3))]
  (prn (.get fut)))
When I call shutdown on the thread pool executor then the script shuts down I guess this would be a breaking change to bb really.

I would revisit this if people were adamant about changing this behavior.

The workaround doesn't seem too tricky, although I wouldn't have figured it out for awhile if I didn't see it in another sample code - maybe just a note would suffice, or somehow tie to httpkit's start call in stderr