The Amazing Speed of Clojure Development

Finch Experiments

This morning I didn't have anything special to do, so I made two new endpoints for an existing service that I've got for one of my projects. They were really specced out as independent services, but as I looked at them, I knew each were a dozen lines of clojure - tops, and with the compojure library, I could easily add the routes to the existing service, and get everything I needed with a minimal level of effort.

The endpoint was all about adding a historical retrieval to the analytics project that I've been working on. Because I've already been storing these experiment reports in a database, it was easy to write a simple function:

  (defn load-nearest-report
    "Function to load the _nearest_ copy of the Panopticon-formatted
    report from the historical storage for the provided experiment name.
    This is for those times that you want the report for an experiment
    at a specific time."
    [expr-name gen]
    (let [row (dis/query
                ["select id
                    from finch_experiments
                   where experiment=?
                     and generated_at < ?
                   order by generated_at desc
                    limit 1" expr-name (to-timestamp gen)]
                  :result-set-fn first)
          exp-id (:id row)]
      (if exp-id (load-report exp-id))))

Now I've used HoneySQL, and it's nice, but I've found that for me - it's often just plain faster for me to write SQL as that's what I think in. For others, there are a lot of tools, but for me, this is about as easy as it gets - write a SQL statement - add in the arguments, set a result set function, and then load the report (code we already had).

Done. Easy.

The endpoint was just as easy:

  (GET "/experiments/:exp-name/as-of" [exp-name & opts]
       (let [asof (from-long (or (nil-if-zero (parse-long (:timestamp opts)))
                                 (to-long (now))))
             rpt (load-nearest-report exp-name asof)]
         (return-json (if (is-empty? rpt)
                        (-> (select-keys rpt [:timestamp :experiment
                                              :name :updated_at :version
                                              :days_live :origin])
                          (assoc :experiment exp-name :state "stopped")
                          (remove-nil-keys))
                        rpt))))

again, we had a lot of the functions written as part of the underlying support library, or as part of the analytics library - which is the beauty of the clojure style - make it all functions, make them simple and composable, and then it's easy to use them over and over again.

The second service is really a server-side mimicking of the internals of a client library that is at the core of The Shop's A/B testing suite. The idea is to be able to reliably, and quickly, decide if a user should be exposed to the control, or one of the experiment variants. And then make sure that this follows them time after time so that the experience is consistent.

The code for this is a little bigger, but it's because we're parsing the config data and mapping the persistent UUID into a bucket, etc.:

This is probably more convoluted than it needs to be, but the structure of the experiment configuration data isn't really great, but it's not bad. Still... this works and gives us a beautiful endpoint to use.

It's amazing what you can build in a morning with clojure and some good tools.