June 15, 2018

Duct and Redis

I was showing duct to a friend not versed into Clojure at all, and he told me that he did not understand what was going on. How he was supposed to add redis to the stack, that he saw the duct redis module but didn’t even know where to start from there.

Duct is super awesome, Clojure is too obviously, the litterature on duct is not really thorough on the web, you have some wiki pages on the duct github, some posts on James Reeves’ blog and obviously duct documentation which is really good but can be complex for a Clojure beginner, even if we don’t take into account the fact that you need to understand what ataraxy and a lot of other libraries do.

So I thought I would write a small how-to to get you going with a simple duct project and a redis database, that should explain what’s going on and how things connects.

Duct

As explained in their GitHub project:

Duct is a highly modular framework for building server-side applications in Clojure using data-driven architecture.

It is similar in scope to Arachne, and is based on Integrant. Duct builds applications around an immutable configuration that acts as a structural blueprint. The configuration can be manipulated and queried to produce sophisticated behavior.

Duct comes with a lein generator that will let you get started pretty quickly by adding functionality like API middlewares, ataraxy a specific clojure router, ClojureScript, a PostgreSQL component, and so on.

But for a beginner it can be hard to grasp at the beginning how these modules play together, and how the fact that Duct is a highly modular framework matters.

Duct & Redis

Duct enables access to redis through the use of its duct/database.redis.carmine module.

As you can see from the README of this module it says that it provides a Boundary record that holds connection options for Carmine, and a multimethod :duct.database.redis/carmine that initiates thoses options into the said Boundary.

What are we going to build?

The goal of this article is to show you how to create a simple API that when called on /example will reply a JSON containing information stored in a Redis database.

It will show how to scaffold an example application with the lein duct generator, and how to modify it step by step to:

  • add a connection to Redis
  • retrieve these information at runtime
  • use them to connect to redis and fetch some data
  • return them to the client
  • make the tests pass by stubbing our Boundary whith shrubbery
  • make sure that it runs from the REPL and from lein run (or in production)
  • and finally how things connects in this highly modular framework

Installation

We’ll start with lein new duct my-project +api +example +ataraxy and we’ll update the project.clj file to add the dependency to database.redis.carmine and to shrubbery for some testing at the end:

project.clj

(defproject my-project "0.1.0-SNAPSHOT"
  ...
  :dependencies [[org.clojure/clojure "1.9.0"]
                 [duct/core "0.6.2"]
                 [duct/module.logging "0.3.1"]
                 [duct/module.web "0.6.4"]
                 [duct/module.ataraxy "0.2.0"]
                 [duct/database.redis.carmine "0.1.0"]  ; <--
                 [com.gearswithingears/shrubbery "0.4.1"]] ; <--
  :plugins [[duct/lein-duct "0.10.6"]]
  ...

Then we need to update our dev/resources/dev.edn file to configure the options to access redis:

dev/resources/dev.edn

{:duct.core/environment :development
 :duct.core/include ["my-project/config"]

 :duct.database.redis/carmine
 {:spec {:host "127.0.0.1", :port 6379}}}

Finally we need to modify the resources/config.edn so that the generated handler (the +example option) knows about redis:

resources/config.edn

{:duct.core/project-ns  my-project
 :duct.core/environment :production

 :duct.module/logging {}
 :duct.module.web/api {}

 :duct.module/ataraxy
 {[:get "/example"] [:example]}

 :my-project.handler/example
 {:db #ig/ref :duct.database.redis/carmine} ; <--
}

Please take special attention to the :db part. It is the key that we’ll extract from the option in the following part of the tutorial.

Data

Since we want to read data from redis, just use redis-cli to add some data to our redis, so that we’ll be able to read them from our duct application later on.

$ redis-cli sadd persons Rich Stuart James Yogthos
(integer) 4
$ redis-cli smembers persons
1) "Rich"
2) "James"
3) "Stuart"
4) "Yogthos"

We just created a set whose key is named persons in Redis having 4 members.

Tinker

Ok now just run lein repl and then when the repl is ready, type (dev), then (go).

$ lein repl
nREPL server started on port 64229 on host 127.0.0.1 - nrepl://127.0.0.1:64229
REPL-y 0.3.7, nREPL 0.2.13
Clojure 1.9.0
Java HotSpot(TM) 64-Bit Server VM 1.8.0_111-b14
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=> (dev)
:loaded
dev=> (go)
:duct.server.http.jetty/starting-server {:port 3000}
:initiated
dev=>

Now you can hit http://localhost:3000/example and you should get the example created by the lein plugin at the very beginning of this small tutorial.

$ curl http://localhost:3000/example
{"example":"bar"}

Now bear in mind that if you modify the source code of your project, you need to get back in the repl and type (reset) so that theses changes are reflected.

Code

Boundary

Create a new file src/my-project/boundary/person_db.clj with the following content:

src/my-project/boundary/person_db.clj

(ns my-project.boundary.person-db
    (:require [duct.database.redis.carmine]
              [taoensso.carmine :as car :refer (wcar)]))

(defprotocol PersonDatabase
    (list-persons [db]))
    
(extend-protocol PersonDatabase
    duct.database.redis.carmine.Boundary
    (list-persons [db]
        (let [conn (:conn-opts db)]
            (car/wcar conn 
                (car/smembers "persons")))))    

In this file we require the carmine library and the duct.database.redis.carmine module.

We then define a protocol named PersonDatabase which contains a function list-persons which needs a db to operate on.

Finally we extend this protocol using the duct.database.redis.carmine.Boundary and we implement the list-persons function.

If you remember in your dev.edn file we declared:

 :duct.database.redis/carmine
 {:spec {:host "127.0.0.1", :port 6379}}}

Extending the protocol using duct.database.redis.carmine.Boundary will gave us access to the following map {:spec {:host "127.0.0.1", :port 6379}} in which we want to extract the :spec key that Carmine needs.

All there is to do is passing this to car/wcar and a body to execute (here (car/smembers "persons") which will run SMEMBERS persons on redis side).

Note: we extract the spec needed by Carmine from the duct.database.redis.carmine.Boundary using the :conn-opts key. The spec is being embedded in this :conn-opts key in the implementation of duct/database.redis.carmine, you can see it in the duct/database/redis/carmine.clj source file:

(defrecord Boundary [conn-opts])
    
(defmethod ig/init-key :duct.database.redis/carmine [_ conn-opts]
  (->Boundary conn-opts))

That’s it for the boundary part.

Handler

All is well, we should now retrieve these information and return them in the handler so that it gets converted into JSON.

Open the src/my-project/handler/example.clj and modify the code like this:

src/my-project/handler/example.clj

(ns my-project.handler.example
  (:require [ataraxy.core :as ataraxy]
            [ataraxy.response :as response] 
            [integrant.core :as ig]
            [my-project.boundary.person-db :as person-db]))

(defmethod ig/init-key :my-project.handler/example [_ {:keys [db]}]
  (fn [{[_] :ataraxy/result}]
    [::response/ok {:persons (person-db/list-persons db)}]))

We just changed {:example "data"} to {:persons ...} so that we return the JSON structure we wants. Additionnaly we changed [_ options] to [_ {:keys [db]}] in order to extract the db configuration from the option so that we can pass it to our Boundary.

Ok, go back in your REPL and type (reset).

dev=> (reset)
:reloading (my-project.boundary.person_db my-project.handler.example my-project.handler.example-test my-project.main dev user)
:resumed
dev=>

You can now hit http://localhost:3000/example to verify that it works.

$ curl http://localhost:3000/example
{"persons":["Rich","James","Stuart","Yogthos"]}

Testing

Back in your REPL type (test), you should be greeted with some stacktrace.

dev=> (test)

ERROR in my-project.handler.example-test/smoke-test)
Uncaught exception, not in assertion.
expected: nil
  actual:
java.lang.IllegalArgumentException: No implementation of method: :list-persons of protocol: #'my-project.boundary.person-db/PersonDatabase found for class: nil
                    clojure.core/-cache-protocol-fn         core_deftype.clj:  583
       my-project.boundary.person-db/eval22226/fn/G            person_db.clj:    5
         my-project.handler.example/eval22272/fn/fn              example.clj:   10
                 my-project.handler.example-test/fn         example_test.clj:   10
                           clojure.test/test-var/fn                 test.clj:  716
                              clojure.test/test-var                 test.clj:  716
                eftest.runner/test-vars/test-var/fn               runner.clj:   54
                       clojure.test/default-fixture                 test.clj:  686
                   eftest.runner/test-vars/test-var               runner.clj:   50
              eftest.runner/wrap-test-with-timer/fn               runner.clj:   30
                                                ...
                                 clojure.core/apply                 core.clj:  657
                        clojure.core/with-bindings*                 core.clj: 1965 (repeats 2 times)
                                                ...
                                 clojure.core/apply                 core.clj:  661
                          clojure.core/bound-fn*/fn                 core.clj: 1995
                                                ...
                            clojure.core/pmap/fn/fn                 core.clj: 6942
                clojure.core/binding-conveyor-fn/fn                 core.clj: 2022
                                                ...
                java.util.concurrent.FutureTask.run          FutureTask.java:  266
  java.util.concurrent.ThreadPoolExecutor.runWorker  ThreadPoolExecutor.java: 1142
 java.util.concurrent.ThreadPoolExecutor$Worker.run  ThreadPoolExecutor.java:  617
                               java.lang.Thread.run              Thread.java:  745


1/1   100% [==================================================]  ETA: 00:00

Ran 1 tests in 0,087 seconds
1 assertion, 0 failures, 1 error.
{:test 1, :pass 0, :fail 0, :error 1, :type :summary, :duration 86.557333}

If you open the test/my-project/example_test.clj you will see the following code:

test/my-project/example_test.clj

(ns my-project.handler.example-test
  (:require [clojure.test :refer :all]
            [integrant.core :as ig]
            [ring.mock.request :as mock]
            [my-project.handler.example :as example]))

(deftest smoke-test
  (testing "example page exists"
    (let [handler  (ig/init-key :my-project.handler/example {})
          response (handler (mock/request :get "/example"))]
      (is (= :ataraxy.response/ok (first response)) "response ok"))))

You can see that the handler is being created using ig/init-key but the options are empty ({}), this is where we’d like to inject our mocked redis that we’re going to create just now using shrubbery.

Create a new file test/my-project/boundary/person_db_test.clj:

test/my-project/boundary/person_db_test.clj

(ns my-project.boundary.person-db-test
    (:require [my-project.boundary.person-db :refer :all]
              [shrubbery.core :refer :all]))

(def mock-list-persons ["Foo" "Bar" "Bazz"])

(def mock-redis
    (stub PersonDatabase
        {:list-persons mock-list-persons}))      

We stub PersonDatabase by redefining the list-persons function so that it return always 3 members Foo, Bar and Bazz.

Now get back to test/my-project/example_test.clj and require this mock and inject it as configuration option for the handler.

Finally add a new test so that we can check that redis was correctly mocked.

(ns my-project.handler.example-test
  (:require [clojure.test :refer :all]
            [integrant.core :as ig]
            [ring.mock.request :as mock]
            [my-project.handler.example :as example]
            [my-project.boundary.person-deb-test :refer [mock-redis mock-list-persons]]))

(deftest smoke-test
  (testing "example page exists"
    (let [handler  (ig/init-key :my-project.handler/example {:db mock-redis})
          response (handler (mock/request :get "/example"))
          [status {:keys [persons]}] response]
      (is (= :ataraxy.response/ok status) "status is ok")
      (is (= persons mock-list-persons) "redis is mocked"))))

Go bak to your REPL and type (reset) then (test):

dev=> (test)

1/1   100% [==================================================]  ETA: 00:00

Ran 1 tests in 0,003 seconds
2 assertions, 0 failures, 0 errors.
{:test 1, :pass 2, :fail 0, :error 0, :type :summary, :duration 3.241681}
dev=>

Now you have 1 test with 2 assertions passing.

Congratulations!

RUnning

Quit the REPL and run lein run. What happens ?

$ ~/Dev/tools/lein/lein run
Exception in thread "main" clojure.lang.ExceptionInfo: Missing definitions for refs: :duct.database.redis/carmine {:reason :integrant.core/missing-refs, :config {:duct.handler.static/method-not-allowed {:body {:error :method-not-allowed}}, :duct.logger/timbre {:level :info, :appenders #:duct.logger.timbre{:println #integrant.core.Ref{:key :duct.logger.timbre/println}}}, :duct.module/ataraxy {[:get "/example"] [:example]}, :duct.middleware.web/format {}, :duct.core/handler {:router #integrant.core.Ref{:key :duct.router/ataraxy}, :middleware [#integrant.core.Ref{:key :duct.middleware.web/not-found} #integrant.core.Ref{:key :duct.middleware.web/format} #integrant.core.Ref{:key :duct.middleware.web/defaults} #integrant.core.Ref{:key :duct.middleware.web/log-requests} #integrant.core.Ref{:key :duct.middleware.web/log-errors} #integrant.core.Ref{:key :duct.middleware.web/hide-errors}]}, :duct.router/ataraxy {:routes {[:get "/example"] [:example]}, :handlers {:ataraxy.error/unmatched-path #integrant.core.Ref{:key :duct.handler.static/not-found}, :ataraxy.error/unmatched-method #integrant.core.Ref{:key :duct.handler.static/method-not-allowed}, :ataraxy.error/missing-params #integrant.core.Ref{:key :duct.handler.static/bad-request}, :ataraxy.error/missing-destruct #integrant.core.Ref{:key :duct.handler.static/bad-request}, :ataraxy.error/failed-coercions #integrant.core.Ref{:key :duct.handler.static/bad-request}, :ataraxy.error/failed-spec #integrant.core.Ref{:key :duct.handler.static/bad-request}, :example #integrant.core.Ref{:key :my-project.handler/example}}, :middleware {}}, :duct.middleware.web/log-requests {:logger #integrant.core.Ref{:key :duct/logger}}, :duct.middleware.web/defaults {:params {:urlencoded true, :keywordize true}, :responses {:not-modified-responses true, :absolute-redirects true, :content-types true, :default-charset "utf-8"}}, :duct.server.http/jetty {:port 3000, :handler #integrant.core.Ref{:key :duct.core/handler}, :logger #integrant.core.Ref{:key :duct/logger}}, :duct.handler.static/internal-server-error {:body {:error :internal-server-error}}, :duct.middleware.web/hide-errors {:error-handler #integrant.core.Ref{:key :duct.handler.static/internal-server-error}}, :duct.middleware.web/log-errors {:logger #integrant.core.Ref{:key :duct/logger}}, :my-project.handler/example {:db #integrant.core.Ref{:key :duct.database.redis/carmine}}, :duct.handler.static/not-found {:body {:error :not-found}}, :duct.router/cascading [], :duct.middleware.web/not-found {:error-handler #integrant.core.Ref{:key :duct.handler.static/not-found}}, :duct.core/environment :production, :duct.module/logging {}, :duct.middleware.web/stacktrace {}, :duct.handler.static/bad-request {:body {:error :bad-request}}, :duct.logger.timbre/println {}, :duct.module.web/api {}, :duct.core/project-ns my-project}, :missing-refs (:duct.database.redis/carmine)}, compiling:(/private/var/folders/pn/jcqmh8s53hn_wtkjvlwnsv300000gn/T/form-init2116886874611819468.clj:1:125)
    at clojure.lang.Compiler.load(Compiler.java:7526)
    at clojure.lang.Compiler.loadFile(Compiler.java:7452)
    at clojure.main$load_script.invokeStatic(main.clj:278)
    at clojure.main$init_opt.invokeStatic(main.clj:280)
    at clojure.main$init_opt.invoke(main.clj:280)
    at clojure.main$initialize.invokeStatic(main.clj:311)
    at clojure.main$null_opt.invokeStatic(main.clj:345)
    at clojure.main$null_opt.invoke(main.clj:342)
    at clojure.main$main.invokeStatic(main.clj:424)
    at clojure.main$main.doInvoke(main.clj:387)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.lang.Var.applyTo(Var.java:702)
    at clojure.main.main(main.java:37)

Well, that was unexpected. The stacktrace can seem a bit obscure, but if you read it carefully it says that it cannot find the information for :duct.database.redis/carmine and this is true!

We only defined it in our dev/src/dev.edn file. We need to define something in our resources/my-project/config.edn.

resources/my-project/config.edn

:duct.database.redis/carmine
{:spec {:host "127.0.0.1", :port 6379}}

But that’s not right, I don’t know for sure that in production this is the correct information…

What we want is to use #duct/env which will read the information from the environment variables.

resources/my-project/config.edn

{:duct.core/project-ns  my-project
 :duct.core/environment :production

 :duct.module/logging {}
 :duct.module.web/api {}

 :duct.module/ataraxy
 {[:get "/example"] [:example]}

 :duct.database.redis/carmine
 {:spec 
    {:host #duct/env "REDIS_HOST", 
     :port #duct/env ["REDIS_PORT" Int]}}
 
 :my-project.handler/example
 {:db #ig/ref :duct.database.redis/carmine}
}

Then run REDIS_HOST=127.0.0.1 REDIS_PORT=6379 ~/Dev/tools/lein/lein run

It should now work correctly :)

If you’re still unsure, set a random port for REDIS_PORT and retry, you should have a stacktrace saying something like clojure.lang.ExceptionInfo: Carmine connection error

What to remember?

Duct serves web requests using duct/module.web, and destructures them using duct/module.ataraxy.

By importing the duct/database.redis.carmine module, Duct gives us a redis Boundary (using Carmine) that we can use to retrieve the redis connection option in order to implement our business Boundary (here PersonDatabase).

It is good practice to stub (mock) your boundaries so that we can use them in unit tests.

Developping using the REPL is cool and let us tinker with the code easily.

Having configuration in production coming from environment variables is good practice, duct has us covered with #duct/env, and you can override it in development in your dev.edn config file.

Alexandre Grison - //grison.me - @algrison