/frodo

A lein plugin to start a Ring server via configuration in Nomad

Primary LanguageClojure

Lein-Frodo (DEPRECATED)

I’m no longer maintaining Frodo (not that it needed much maintenance anyway!) because the Clojure-world has moved on. I’d recommend checking out Stuart Sierra’s Component library, or my own Yo-yo library.

A Leiningen plugin to start a web server (backed by http-kit) easily via configuration in Nomad.

Benefits

  • Easy setup of a web server that you can nREPL into
  • Easy integration with Stuart Sierra’s (@stuartsierra) ‘Reloaded’ workflow
  • Ability to open up an nREPL even if you have compiler errors - you can use the REPL to find them without restarting the JVM each time

Dependency

Include lein-frodo as a plugin in your project.clj:

:plugins [[jarohen/lein-frodo "0.4.2"]]

Frodo versions earlier than 0.4.1 are not compatible with Nomad 0.7.0 or later, please update your Frodo version.

Changes

Now moved to CHANGES.org.

Why?

Well, I already use Nomad for most of my configuration. I configure various environments using Nomad’s environment functionality, and wanted the web server to be configured in the same way.

In each project, I found myself writing the same boilerplate startup code - reading the port details from my configuration in order to start nREPL and a web server on the relevant ports for the environment.

With Frodo, it’s possible to start web applications with:

NOMAD_ENV=<<environment>> lein frodo

and have the ports vary by environment.

For more details about what’s possible with Nomad, please see its project page.

(I did use lein-ring for a bit but, while it is a great plugin, I’d much prefer all my configuration in one place - hence taking the time to write this!)

“About the name…?”

Yes, it’s corny, I’m sorry! I did toy with lein-nomad-ring, and various permutations, but none of them really seemed to bring together Ring and Nomad in the way lein-frodo did. Alternatives gratefully received!

Getting started

Creating a new project

The easiest way to start a Frodo project is by using the SPLAT Lein template - most, if not all, of the below is done for you.

Migrating an existing project

First, create a Nomad configuration file somewhere on your classpath, and add a :frodo/config key, as follows:

;; *project-root*/resources/config/nomad-config.edn:

{:frodo/config {:nrepl {:port 7888}
                :web {:port 3000
                      :app myapp.web/app}}}

Frodo expects apps to provide an instance of its App protocol, containing two functions: start! and stop!.

(ns myapp.web
  (:require [frodo.web :refer [App]]
            [compojure.core :refer [routes GET]]
            [ring.util.response :refer [response]]))

(defn api-routes [db-conn]
  (routes
    (GET "/items" [] (response (get-items db-conn)))))

;; recommended as of 0.3.0
(def app
  (reify App

    (start! [_]
      (let [db-conn (connect-db! ...)]
        {:db-conn db-conn
         :scheduler (start-scheduler! db-conn)
         ;; Apps must return a :frodo/handler key
         :frodo/handler (api-routes db-conn)}))

    (stop! [_ system]
      (stop-scheduler! (:scheduler system))
      (disconnect-db! (:db-conn system)))))

Frodo expects the start! function to return a system map containing at least a :frodo/handler key. When the server is stopped or restarted, this system map is passed to the stop! function for you to tear down any state/close resources etc (in line with the ‘Reloaded’ workflow - more details below).

When you’ve created your App, add an entry in your project.clj to tell Frodo where your config file is:

:frodo/config-resource "config/nomad-config.edn"

To run the Ring server, run:

lein frodo

Pre 0.3.0 handlers

Frodo supports two alternatives to providing an ‘app’, for backwards compatibility: :handler-fn and :handler. ‘Handler functions’ are 0-arg functions that return a Ring handler, whereas the simpler ‘handler’ is a static handler.

These may be removed in a future version of Frodo.

;; ---- config/nomad-config.edn ----

{:frodo/config {:nrepl {:port 7888}
                :web {:port 3000
                      ;; any one of :app, :handler-fn or :handler is req'd
                      :app myapp.web/app
                      :handler-fn myapp.web/make-handler
                      :handler myapp.web/handler}}}

;; ---- myapp/web.clj ----

;; like the 'start!' function of 'app' - no corresponding 'stop!' fn
;; though.
(defn make-handler []
  (let [db-conn (connect-db! ...)]
    (api-routes db-conn)))

;; static handler
(def handler
  (routes
    (GET "/" [] (response "Hello world!"))))

“You say you use multiple environments?”

Yes - you can do this in the traditional Nomad way:

;; *project-root*/resources/config/nomad-config.edn:

{:nomad/environments {"dev"
                      {:frodo/config {:nrepl {:port 7888}
                                      :web {:port 3000}}}

                      "prod"
                      {:frodo/config {:nrepl {:port nil}
                                      :web {:port 4462}}}}}

Then, start your application with either:

NOMAD_ENV=dev lein frodo

or:

NOMAD_ENV=prod lein frodo

This is just the simplest multiple environment configuration - there are many more possibilities on the Nomad project page.

Passing options to HTTP-kit

You can pass options to HTTP-kit by specifying a :http-kit/options key in the :web map:

{:frodo/config {:nrepl {...}
                :web {:port ...
                      :handler-fn ...
                      :http-kit/options {:thread 100}}}}

For a full list of the options that HTTP-kit accepts, please see here.

Frodo, Reloaded.

As of 0.2.6, you can develop web-apps in Frodo using Stuart Sierra’s ‘Reloaded’ workflow. I won’t go into huge detail about the pattern itself (his blog is very informative and plenty else has been written about the benefits!) but I do find it a great way to get a ‘fresh’ state without having to restart the JVM.

Essentially:

  • Set up your system state and resources in the start! function (for an App).
  • Ensure that your code doesn’t contain any def’s or defonce’s (and preferably no defroutes’s - replace these with (defn my-routes [] (routes ...))) so that all the state can be reloaded.
  • Tear down any state and close resources in the stop! function
  • Call (reload-frodo!) from the user namespace to throw out the old state and start afresh. This will stop the web server, refresh any changed code files, and restart the web server, without restarting the JVM. This typically takes less than a second.
  • Call (frodo-instance) to get access to the currently running instance.

To restart the web server from your REPL:

user> (reload-frodo!)

;; Stopping web server.
;; :reloading (tetris.multiplayer tetris.handler)
;; Starting web server, port 3000
;; => nil

UberJAR support (beta)

To build a batteries-included JAR file of your application, run lein frodo uberjar.

Future features?

  • SSL? I’m not sure how many people use SSL within Clojure - from what I can tell most people sit it behind an nginx/httpd proxy. If you want to include SSL support, please feel free to submit a pull request.
  • uberwar? Again, I don’t use this, but if you do and you care enough to write a patch, it’ll be gratefully received!

Pull requests/bug reports/feedback etc?

Yes please, much appreciated! Please submit via GitHub in the traditional manner. (Or, if it fits into 140 chars, you can tweet @jarohen)

Thanks

  • Big thanks to James Reeves for his lein-ring project (amongst everything else!) from which I have plundered a couple of ideas and snippets of code. Also, thanks for the general help and advice.
  • Thanks to Stuart Sierra for writing up his ’Reloaded’ workflow - a great way of thinking about server-side state in Clojure

License

Copyright © 2013, 2014 James Henderson

Distributed under the Eclipse Public License, the same as Clojure.