Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
A little utility to allow simple redeployment of Clojure web servers with zero downtime, and without the need for a proxy or load balancer. Just wrap any port-binding calls, and the utility will auto kill pre-existing servers as necessary. *nix only. Based on the blog post by Feng Shen,
;; (require '[clojure.string :as str] '[ :as shell] '[taoensso.timbre :as timbre])
(defn with-free-port!
"Attempts to kill any current port-binding process, then repeatedly executes
nullary `bind-port!-fn` (which must return logical true on successful
binding). Returns the function's result when successful, else throws an
exception. *nix only.
This idea courtesy of Feng Shen, Ref."
[port bind-port!-fn & {:keys [max-attempts sleep-ms]
:or {max-attempts 50
sleep-ms 150}}]
(let [binder-pid (str/trim (:out (shell/sh "lsof" "-t" "-sTCP:LISTEN"
(str "-i:" port))))]
(when-not (str/blank? binder-pid)
(timbre/warn "Attempting to kill process" binder-pid "to free port" port)
(let [kill-resp (shell/sh "kill" binder-pid)]
(when-not (= (:exit kill-resp) 0)
(throw (Exception. (str "Failed to kill process " binder-pid
" while trying to free port " port ": "
(:err kill-resp)))))))
(loop [attempt 1]
(when (> attempt max-attempts)
(throw (Exception. (str "Failed to bind to port " port " within "
max-attempts " attempts ("
(* max-attempts sleep-ms) "ms)"))))
(let [result (try (bind-port!-fn) (catch _))]
(if result
(do (timbre/info (str "Bound to port " port " after "
attempt " attempt(s)"))
(do (Thread/sleep sleep-ms)
(recur (inc attempt))))))))
(comment (with-free-port! 8080 (fn [] (ring.adapter.jetty/run-jetty my-handler {:port 8080 :join? false}))))
;; Example
(defn- die-with "Terminates app after logging given message."
([message] (timbre/fatal message) (System/exit 1))
([exception message] (timbre/fatal exception message) (System/exit 1)))
(defonce servers (atom {}))
(defn- start-server-or-die!
[server-name port start-server!-fn]
(when (and port (not (@servers server-name)))
(timbre/info (str "Attempting to start " server-name " server..."))
(try (when-let [;; Kill other binding processes & throw an exception on
;; binding failure:
server (with-free-port! port start-server!-fn)]
(swap! servers assoc server-name server)
(timbre/report (str server-name " server is running on port " port)))
(catch Exception e
(die-with e (str "Failed to start " server-name " server"))))))
(comment (start-server-or-die! :jetty 8080
(fn [] (ring.adapter.jetty/run-jetty my-handler {:port 8080 :join? false}))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.