Getting a feel for closeables
2023-11-05
The other day, I stumbled upon this article by Maciej Szajna which presents a minimalist way to declare and manage runtime state in your Clojure programs. Having used Components, Integrant and then Clip, this felt almost like cheating. Can something so simple actually work? Well, it actually does pretty well. Let's illustrate that by implementing a web server, pretty barebone, but with some "reloaded workflow". This won't be much more than a special case for the pattern demonstrated in Maciej's article, but it will serve as a foundation to build upon in future posts.
A barebone webserver
If you want to evaluate the code for yourself, you can find the following examples in this repository.
First, let's add the closeable
helper:
defn closeable
(identity))
([value] (closeable value
([value close]reify
(deref [_] value)
clojure.lang.IDeref ( java.io.Closeable (close [_] (close value)))))
Now, let's add a very basic webserver, that shows only one page. It increments and displays a counter each time it is served:
require '[ring.adapter.jetty :refer [run-jetty]])
(
defn run-with-webserver [config f]
(with-open
(atom 42))
[counter (closeable (fn [_req]
handler (closeable (:status 200
{:body (str "Counter: "
swap! @counter inc))}))
(@handler {:port (:port config)
webserver (closeable (run-jetty :join? false})
%))]
#(.stop @webserver))) (f
Compared to Maciej's article, you may notice 2 main differences in this example:
It does not return a function that closes over the configuration, and it does not bother building an associative map with every binding declared in
with-open
.run-with-webserver
is much more specific than the genericwith-my-system
. The main side effect of calling this function is to open up a port on the host where it is run, so I prefer to narrow the meaning to reflect that.
We can see it in action by evaluating the following expression:
:port 54321}
(run-with-webserver {fn [_webserver]
(println "The server is live:"
(slurp "http://localhost:54321")))) (
It should print The server is live: Counter: 43
in your
REPL (among other log statements). That is well and good, but if you try
to access http://localhost:54321
from your browser, you'll see that the server is not actually running
anymore. As explained in Maciej's
article, once the function we pass to
run-with-webserver
returns, the opened resources are
released. In order to keep the server running indefinitely, we can use
.join
on the Jetty Server.
:port 54321} #(.join %)) (run-with-webserver {
NB: Depending on your tooling, evaluating the previous expression can block your REPL. You'll need to interupt the evaluation to stop the webserver.
Testing
It felt odd at first having the "run" function not do its task indefinitely. After all, Clojure was made for situated programs, long running processes tangled with outside world. But it makes it a lot easier to work with our system in various ways. Testing is effortless for example:
require '[clojure.test :refer [deftest is run-tests]])
(
deftest test-webserver
(let [url "http://localhost:12345"]
(is (thrown? java.net.ConnectException (slurp url)))
(:port 12345}
(run-with-webserver {fn [_webserver]
(is (= (slurp url) "Counter: 43"))
(is (= (slurp url) "Counter: 44"))))
(is (thrown? java.net.ConnectException (slurp url)))))
(
comment (run-tests)) (
The famous "reloaded" workflow
Finally, let's add some convenient handles (in user.clj
)
to play with our webserver from the REPL.
defonce *live-server (atom (future ::not-initialized-yet)))
(
defn start! []
(reset! *live-server (future (run-with-webserver {:port 54321}
(join %)))))
#(.defn stop! []
(future-cancel @*live-server))
(
comment (start!)
( (stop!))
After evaluating this code and calling (start!)
, you
should be able to visit http://localhost:54321 and see the
counter for yourself. But do not evaluate this by hand! Your editor
probably has some integration with tools.namespace via a plugin. For
example for Emacs and Cider, I usually declare a
.dir-locals.el
at the root of the projet with the
following:
((clojure-mode . ((cider-ns-refresh-before-fn . "user/stop!")
(cider-ns-refresh-after-fn . "user/start!"))))
I can then call cider-ns-refresh
to stop my system,
refresh the namespaces, and start the system again. Notice that I still
defonce
the reference to the live server, so that I do not
lose it if I eval the whole buffer. I also provide a dummy future in
that reference so that I can call cider-ns-refresh
to start
my system the first time.
Conclusion
Even though I do not yet have had a lot experience with this approach, there are already positive reports of its use, so I'm eager to use it in my projects going forward. In the next post, we'll explore a slightly meatier example of web server.