Liking cljdoc? Tell your friends :D

Reloading at the REPL

As Clojure developers, we prefer to live at the REPL, loading and reloading code into the REPL for a lightning fast feedback loop.

It’s common to reload a namespace, and use the REPL (possibly via IDE support) to re-evaluate expressions in our code and see the immediate change. This is true for Pedestal apps as well …​ we should be able to make changes and see the effects of those changes immediately. But as we’ll see, in practice, accomplishing that takes a little extra structure.

We’re going to show examples of the live reloading failures, then show how Pedestal solves them.

Starting Point

Let’s start with yet another basic, "Hello World" application.

(ns org.example.hello
  (:require [io.pedestal.connector :as conn]
            [io.pedestal.http.http-kit :as hk]))

(defn hello-handler [_request] (1)
  {:status 200
   :body "Hello, World!"})

(def routes
  #{["/hello" :get hello-handler :route-name ::hello]}) (2)

(defn create-connector []
  (-> (conn/default-connector-map 9999)
      (conn/with-default-interceptors)
      (conn/with-routes routes)                             (3)
      (hk/create-connector nil)))

(defonce *connector (atom nil)) (4)

(defn start []
  (reset! *connector (conn/start! (create-connector))))

(defn stop []
  (conn/stop! @*connector)
  (reset! *connector nil))
1The request map isn’t used here, so we follow the convention of adding a leading underscore; this may also prevent warnings in your IDE.
2Every route must have a unique :route-name and since we are providing a function, hello-handler, it must be explicitly specified.
3api:with-routes[ns=io.pedestal.connector] is a macro; it adds a routing interceptor to the connector map.
4To survive reloading of this namespace, the running connector is stored in an Atom.

With that in place, evaluate (start) to create and start the connector.

You can now use your favorite HTTP client (such as HTTPIe) to access the /hello route:

> http :9999/hello
HTTP/1.1 200 OK
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: text/plain
Date: Fri, 18 Nov 2022 23:41:49 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: DENY
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 1; mode=block

Hello, World!

So far, so good. You can also see that Pedestal’s default set of interceptors has added a number of security-related headers to the response.

Now we’re ready to make a change. Let’s change the response body to be Hello, Clojure World!. We can change the hello-handler function, reload the namespace via (require :reload 'org.example.hello), and retest with http:

> http -pb :9999/hello (1)
Hello, World!
1The -pb option prints just the response body, omitting the response headers.

That’s strange. It’s as if the code change wasn’t picked up. We can verify that the code change is, in fact, live by evaluating (hello-handler nil) at the REPL:

(hello-handler nil)
=> {:status 200, :body "Hello, Clojure World!"}

What’s happened here is that the value of the org.example.hello/hello-handler Var has changed; it now holds a new Clojure function, one that says "Hello, Clojure World!". However, the routes and create-connector functions captured the value of hello-handler at the time they executed.

We could say ugh, fine and just adapt to a cycle where you reload your REPL code and then restart the connector, but that’s slow and error-prone. We want a fast feedback cycle where that is not necessary. That goal is entirely achievable.

Understanding the Capture Problem

To understand what’s going on here, we must consider a key stage in Pedestal: route expansion. Route expansion is a step during startup where the provided routes are converted from one of a number of specifications (such as the table format used here) into an executable format. A handler in a route is evaluated during expansion; that’s when the hello-handler symbol is resolved to a specific function.

This evaluation and expansion only happens once, and so there’s a capture: the running connector holds a reference to the specific function that existed when the routes were expanded. Reloading a namespace creates a new function value, but the running connector still holds the old one.

There’s actually more than one layer of capture at work:

  1. When the routes were defined, hello-handler was a direct function reference, captured into the routes set.

  2. When create-connector was called, the value of routes was captured into the connector.

  3. Any changes to routes, handlers, or interceptors after the connector is created are invisible.

We need a way to avoid all of these captures, so that incoming requests always reflect the latest code.

Development Mode

Fortunately, the api:with-routes[ns=io.pedestal.connector] macro handles all of this for us, provided development mode is enabled.

with-routes internally uses the api:routes-from[ns=io.pedestal.http.route] macro, which is aware of the Pedestal execution mode. In production mode, routes are expanded once at startup — exactly what you want for performance. In development mode, routes are re-expanded on every incoming request, and all symbols are dynamically re-resolved, so changes to handler functions, routes, and interceptors are picked up immediately.

To enable development mode, set the system property io.pedestal.dev-mode to true when starting the JVM. With the clj tool, this can be done through the deps.edn:

{:paths ["src"]
 :deps {io.pedestal/pedestal.http-kit {:mvn/version "{pedestal-version}"}
        org.slf4j/slf4j-simple {:mvn/version "2.0.17"}}
 :aliases
 {:dev {:jvm-opts ["-Dio.pedestal.dev-mode=true"]}}}

Then start the REPL in development mode via clj -A:dev.

Alternatively, you can set the environment variable PEDESTAL_DEV_MODE=true.

Once development mode is enabled, no code changes are needed. Our existing code, with conn/with-routes, already does the right thing:

(defn create-connector []
  (-> (conn/default-connector-map 9999)
      (conn/with-default-interceptors)
      (conn/with-routes routes) (1)
      (hk/create-connector nil)))
1In development mode, this re-expands routes on every request, dynamically resolving all symbols.

Let’s stop the connector and start fresh, this time with development mode on:

(stop)
(start)

Verify the original behavior:

> http -pb :9999/hello
Hello, World!

Now, change hello-handler to return "Hello, Clojure World!", reload the namespace, and retest:

> http -pb :9999/hello
Hello, Clojure World!

Success! The change was picked up without restarting the connector.

Let’s also change the route path from /hello to /hi, reload the namespace, and try it:

> http -pb :9999/hi
Hello, Clojure World!

That works too. You can add, remove, or otherwise change your routes, update handler functions, add interceptors …​ wherever your development takes you, and those changes will be re-evaluated and re-loaded on each request.

Not For Production

In development mode, route expansion happens on every incoming request. Even a trivial route specification takes a chunk of time to expand, so this would absolutely trash your production server’s throughput. This is why it must be explicitly enabled via the system property or environment variable; by default Pedestal operates in production mode.

In development mode, Pedestal will also write a formatted routing table to the console at startup, and at any later time that the routing table changes:

Routing table:
┌──────┬──────┬───────────────────────────┐
│Method│ Path │           Name            │
├──────┼──────┼───────────────────────────┤
│  :get│/hello│:org.example.hello/hello   │
└──────┴──────┴───────────────────────────┘

This output is especially useful to know the correct route name to pass to api:url-for[ns=io.pedestal.http.route] (to generate application URLs included in responses) or api:response-for[ns=io.pedestal.connector.test] (used in your unit tests).

How does this work under the hood?

with-routes is a macro that calls the routes-from macro internally. In production mode, routes-from simply expands the routes once. In development mode, it generates a zero-argument function that re-expands the routes on each call, using ns-resolve to dynamically look up every symbol. This means that both the route definitions and the handler functions they reference are always resolved to their current values.

Wrap Up

Running code with a long-lived and stateful connector creates its own challenges when coding live at the REPL; in this guide we’ve explained how capturing interferes with live reloading, and shown how Pedestal’s development mode solves the problem automatically through the api:with-routes[ns=io.pedestal.connector] macro.

Just remember to ensure that development mode is not enabled in your production deployments if you want to meet your SLAs (service level agreements)!

Can you improve this documentation? These fine people already did:
Howard M. Lewis Ship, Marduk Bolaños & Howard Lewis Ship
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close