Home

Awesome

cl-etcd

Build Status

The purpose of cl-etcd is to simplify the writing of distributed applications in Common Lisp. etcd is a strongly consistent, distributed key-value store. At it's core is an implementation of the raft consensus algorithm. GC pauses in my lisp implementation of choice make it a poor choice for a lisp-based raft implementation, and so we run etcd as an asynchronous child process under lisp.

The cl-etcd package includes basic get-etcd and watch functions, as well as the convenience macro with-etcd to make it easy to start-up and shut-down your "embedded" etcd node. get-etcd is setf-able so you can easily modify the key-value store the Lisp way.

If you want to be notified about leader state changes on your node, supply the optional :on-leader and :on-follower lambdas, and they will be called when the node becomes a cluster leader or follower respectively.

Etcd is configured for auto-TLS communication between peers, meaning that the inter-node traffic will be encrypted with self-signed certificates. No form of authentication or encryption is currently performed between etcd and the client (the cl-etcd library code), however etcd is configured to only allow connections from localhost.

Here's a trivial example of a single node:

(with-etcd (etcd nil)
  (setf (get-etcd "hello" etcd) "world")
  (get-etcd "hello" etcd))

To be notified on state changes to leader or follower, do this:

(defun become-leader (etcd)
  (print "I'm the leader!"))

(defun become-follower (etcd)
  (print "I'm a follower!"))

(with-etcd (etcd nil :on-leader #'become-leader :on-follower #'become-follower)
  (setf (get-etcd "hello" etcd) "world")
  (get-etcd "hello" etcd))

A single instance is pretty useless, so let's make a 3-node cluster! The second argument to with-etcd is a hashtable of etcd arguments. The easiest way to populate this is through TOML config files. Let's make three config files like so...

config1.ini:

[etcd]
name = "infra0"
initial-advertise-peer-urls = "http://127.0.0.1:2380"
listen-peer-urls = "http://127.0.0.1:2380"
listen-client-urls = "http://127.0.0.1:2379"
advertise-client-urls = "http://127.0.0.1:2379"
initial-cluster = "infra0=http://127.0.0.1:2380,infra1=http://127.0.0.1:2480,infra2=http://127.0.0.1:2580"
host-whitelist = "127.0.0.1"

config2.ini:

[etcd]
name = "infra1"
initial-advertise-peer-urls = "http://127.0.0.1:2480"
listen-peer-urls = "http://127.0.0.1:2480"
listen-client-urls = "http://127.0.0.1:2479"
advertise-client-urls = "http://127.0.0.1:2479"
initial-cluster = "infra0=http://127.0.0.1:2380,infra1=http://127.0.0.1:2480,infra2=http://127.0.0.1:2580"
host-whitelist = "127.0.0.1"

config2.ini:

[etcd]
name = "infra2"
initial-advertise-peer-urls = "http://127.0.0.1:2580"
listen-peer-urls = "http://127.0.0.1:2580"
listen-client-urls = "http://127.0.0.1:2579"
advertise-client-urls = "http://127.0.0.1:2579"
initial-cluster = "infra0=http://127.0.0.1:2380,infra1=http://127.0.0.1:2480,infra2=http://127.0.0.1:2580"
host-whitelist = "127.0.0.1"

Now, in each process, load the appropriate config file:

(defun become-leader (etcd)
  (setf (get-etcd "hello" etcd) "world"))

(let ((config (cdr (assoc :etcd (cl-toml:parse-file "config1.ini")))))
  (with-etcd (etcd config :on-leader #'become-leader :on-follower #'become-follower)
    (sleep 3)
    (get-etcd "hello" etcd)))

As this is a work-in-progress, details may change, and feedback is always welcome.

AG