lib.redis

pooled Redis clients, scripts, events, and lifecycle integration

Create RESP connection pools and compose them with optional local services, event listeners, notifications, and script state.

1    Overview

A Redis client map contains identity, host and port settings, runtime script and listener state, and an active connection pool after startup. Component wrappers coordinate supporting lifecycle steps.

2    Walkthrough

2.1    Create and start a client

(require '[lib.redis :as redis])

(def client
  (redis/client-create
   {:host "localhost"
    :port 6379
    :mode :eval}))

(def started
  (redis/client:start client))

2.2    Inspect and close

(redis/client-string started)
(:pool started)
@(:runtime started)
(redis/client:stop started)

3    Supporting namespaces

lib.redis.event manages listeners and notification loops. lib.redis.script and lib.redis.extension support script registration and command extensions. lib.redis.bench integrates local test services.

4    API



*default* ^

NONE
(def ^:dynamic *default*)
link

client-create ^

[{:keys [id env mode host port], :as m}]
Added 3.0

creates a redis client

v 3.0
(defn client-create
  ([{:keys [id env mode host port] :as m}]
   (let [id   (or id (f/sid))
         mode (or mode :eval)
         m    (merge (conn/test:config)
                     m)]
     (merge m {:id id
               :tag :redis
               :mode mode
               :runtime (atom {:scripts {} :listeners {}})}))))
link
(r/client-create {:id "localhost" :port 17001}) => map?

client-start ^

[{:keys [id host port], :as client}]
Added 4.0

starts the client

v 4.0
(defn client-start
  [{:keys [id host port] :as client}]
  (let [pool (pool/pool {:id id
                         :host host
                         :port port
                         :tag "redis.pool"
                         :path [:redis :pool]})]
    (pool/pool:start (assoc client :pool pool))))
link
(with-redefs [pool/pool (constantly :pool) pool/pool:start (fn [c] (assoc c :started true))] (r/client-start {:id :id :host "h" :port 1})) => (contains {:started true :pool :pool})

client-steps ^

[]
Added 3.0

clients steps for start up and shutdown

v 3.0
(defn client-steps
  ([]
   [{:key :container
     :setup     docker/start-runtime
     :teardown  docker/stop-runtime}
    {:key :bench
     :setup     bench/bench-start
     :teardown  bench/bench-stop}
    {:key :events
     :start event/start:events-redis}
    {:key :notify
     :start event/start:notify-redis
     :stop  event/stop:notify-redis}]))
link
(r/client-steps) => vector?

client-string ^

[{:keys [host port pool]}]
Added 4.0

creates a cliet string

v 4.0
(defn client-string
  [{:keys [host port pool]}]
  (str "#rt.redis.client " (merge {:host host :port port}
                                   (component/info pool))))
link
(r/client-string {:host "h" :port 1 :pool :p}) => string?

client:kill ^

[conn]

NONE
(def ^{:arglists '([conn])} client:kill  (component/wrap-stop  pool/pool:kill  (client-steps)))
link

client:start ^

[conn]

NONE
(def ^{:arglists '([conn])} client:start (component/wrap-start client-start (client-steps)))
link

client:stop ^

[conn]

NONE
(def ^{:arglists '([conn])} client:stop  (component/wrap-stop  pool/pool:stop  (client-steps)))
link


+actions+ ^

NONE
(defonce +actions+
  (atom {}))
link

+default+ ^

NONE
(def +default+
  #{:string :generic :hash :list :expired :stream})
link

+lu+ ^

NONE
(def +lu+)
link

+rlu+ ^

NONE
(def +rlu+ (collection/map-entries (fn [[k v]] [(first v) k]) +lu+))
link

->Listener ^

[type id namespace handler connection]

NONE
(impl/defimpl Listener [type id namespace handler connection]
  :string listener-string)
link

Listener ^

NONE
(impl/defimpl Listener [type id namespace handler connection]
  :string listener-string)
link

action:add ^

[key {:keys [subscribe unsubscribe args wrap], :as m}]
Added 3.0

adds an action from registry

v 3.0
(defn action:add
  ([key {:keys [subscribe unsubscribe args wrap] :as m}]
   (swap! +actions+ assoc key m)))
link
(action:add :test {:a 1}) => (contains {:test {:a 1}})

action:get ^

[key]
Added 3.0

gets action type

v 3.0
(defn action:get
  ([key]
   (or (get @+actions+ key)
       (throw (ex-info "Not found" {:input key})))))
link
(action:add :test {:a 1}) (action:get :test) => {:a 1}

action:list ^

[]
Added 3.0

lists action types

v 3.0
(defn action:list
  ([]
   (keys (keys @+actions+))))
link
(action:list) => seq?

action:remove ^

[key]
Added 3.0

removes an action from registry

v 3.0
(defn action:remove
  ([key]
   (swap! +actions+ dissoc key)))
link
(action:add :test {:a 1}) (action:remove :test) => map?

config:add ^

[redis events] [redis events opts]
Added 3.0

adds config notifications

v 3.0
(defn config:add
  ([redis events]
   (config:add redis events nil))
  ([redis events opts]
   (do (config:set redis (apply conj (config:get redis opts) events) opts)
       (config:get redis opts))))
link
(with-redefs [config:get (constantly #{:string}) config:set (constantly "OK")] (config:add |client| #{:generic})) => #{:string}

config:get ^

[redis] [redis opts]
Added 3.0

gets the config for notifications

v 3.0
(defn config:get
  ([redis]
   (config:get redis nil))
  ([redis opts]
   ((comp events-parse f/strn second)
    (pool/pool:request-single redis ["CONFIG" "GET" "notify-keyspace-events"] opts))))
link
(with-redefs [pool/pool:request-single (constantly ["key" "K$g"])] (config:get |client|)) => #{:string :generic}

config:remove ^

[redis events] [redis events opts]
Added 3.0

removes config notifications

v 3.0
(defn config:remove
  ([redis events]
   (config:remove redis events nil))
  ([redis events opts]
   (let [current (config:get redis)]
     (config:set redis (clojure.set/difference current (set events)))
     (config:get redis opts))))
link
(with-redefs [config:get (constantly #{:string :generic}) config:set (constantly "OK")] (config:remove |client| #{:generic})) => #{:string :generic}

config:set ^

[redis events] [redis events opts]
Added 3.0

sets the config for notifications

v 3.0
(defn config:set
  ([redis events]
   (config:set redis events nil))
  ([redis events opts]
   (let [s    (events-string events)
         opts (cc/req:opts opts)]
     (pool/pool:request-single redis ["CONFIG" "SET" "notify-keyspace-events" (str "K" s)]
                               opts))))
link
(with-redefs [pool/pool:request-single (constantly "OK")] (config:set |client| #{:string})) => "OK"

events-parse ^

[s]
Added 3.0

creates a set of enums from a string

v 3.0
(defn events-parse
  ([^String s]
   (set (keep +rlu+ s))))
link
(events-parse "h$g") => #{:hash :string :generic}

events-string ^

[events]
Added 3.0

creates a string from a set of enums

v 3.0
(defn events-string
  ([events]
   (apply str (keep +lu+ events))))
link
(events-string #{:string :hash :generic}) => "h$g"

has-notify ^

[redis id]
Added 3.0

checks that a given notify listener is installed

v 3.0
(defn has-notify
  ([redis id]
   (boolean (get-in @(:runtime redis) [:listeners :notify id]))))
link
(swap! (:runtime |client|) assoc-in [:listeners :notify :id] {}) (has-notify |client| :id) => true

identity:args ^

[_ args]

NONE
(defn- identity:args [_ args] (collection/seqify args))
link

list-notify ^

[redis]
Added 3.0

lists all notify listeners for a client

v 3.0
(defn list-notify
  ([redis]
   (collection/map-vals #(select-keys % [:pattern :handler])
               (get-in @(:runtime redis) [:listeners :notify]))))
link
(list-notify |client|) => {:id {}}

listener-loop ^

[conn f]
Added 3.0

creates a listener loop

v 3.0
(defn listener-loop
  ([conn f]
   (loop []
     (let [msg (wire/read conn)]
       (f msg)
       (recur)))))
link
(let [seen (atom []) count (atom 0)] (try (with-redefs [wire/read (fn [_] (let [n (swap! count inc)] (if (< n 3) n (throw (ex-info "done" {})))))] (listener-loop :conn #(swap! seen conj %))) (catch clojure.lang.ExceptionInfo _)) @seen) => [1 2]

listener-string ^

[{:keys [type connection], :as listener}]
Added 3.0

string description of a listener

v 3.0
(defn listener-string
  ([{:keys [type connection] :as listener}]
   (let [{:keys [id args raw thread]} connection]
     (str "#" (name type) (:id listener) " "
          {:id id :args args
           :running (and (not (future/future:complete? thread))
                         (component/started? raw))}))))
link
(with-redefs [future/future:complete? (constantly false) component/started? (constantly true)] (listener-string {:type :test :id :listener :connection {:id :conn :args ["in"] :raw :raw :thread :thread}})) => "#test:listener {:id :conn, :args ["in"], :running true}"

listener:add ^

[type redis id input handler] [type {:keys [namespace runtime pool], :as redis} id input handler opts]
Added 3.0

adds a listener to the redis client

v 3.0
(defn listener:add
  ([type redis id input handler]
   (listener:add type redis id input handler {}))
  ([type {:keys [namespace runtime pool] :as redis} id input handler opts]
   (let [{:keys [listeners]} @runtime
         id (or id (keyword (f/sid)))]
     (when-not (get-in listeners [type id])
       (doto (-> (listener:create redis type id input handler)
                 (merge opts))
         (->> (swap! runtime assoc-in [:listeners type id])))))))
link
(with-redefs [listener:create (constantly {:id :id :type :test}) f/sid (constantly "sid")] (listener:add :test |client| :id "in" identity)) => map?

listener:all ^

[{:keys [runtime]}]
Added 3.0

lists all listeners

v 3.0
(defn listener:all
  ([{:keys [runtime]}]
   (mapcat vals (vals (:listeners @runtime)))))
link
(swap! (:runtime |client|) assoc :listeners {:test {:id {:a 1}}}) (listener:all |client|) => [{:a 1}]

listener:count ^

[{:keys [runtime]}]
Added 3.0

counts all listeners

v 3.0
(defn listener:count
  ([{:keys [runtime]}]
   (collection/map-vals count (:listeners @runtime))))
link
(listener:count |client|) => {:test 1}

listener:create ^

[{:keys [namespace pool], :as redis} type id input handler]
Added 3.0

creates a listener

v 3.0
(defn listener:create
  ([{:keys [namespace pool] :as redis} type id input handler]
   (let [[raw-id raw] (cc/pool:acquire pool)
         action (action:get type)
         args   ((action :args) namespace input)
         _      (wire/call raw (into [(action :subscribe)] args))
         connection {:id raw-id
                     :args args
                     :pool pool
                     :thread (future/future
                               (->> ((action :wrap) handler redis)
                                    (listener-loop raw)))
                     :raw raw}]
     (-> {:type type :id id
          :namespace namespace
          :input input
          :handler handler
          :connection connection}
         (map->Listener)))))
link
(with-redefs [cc/pool:acquire (constantly [:raw-id :raw]) action:get (constantly {:args (constantly []) :subscribe "SUB" :wrap (constantly identity)}) wire/call (constantly nil) listener-loop (constantly nil)] (listener:create |client| :test :id "in" identity)) => listener?

listener:get ^

[{:keys [runtime]} type id]
Added 3.0

gets a client listener

v 3.0
(defn listener:get
  ([{:keys [runtime]} type id]
   (get-in (:listeners @runtime)
           [type id])))
link
(listener:get |client| :test :id) => {:a 1}

listener:list ^

[{:keys [runtime]}]
Added 3.0

lists all listeners

v 3.0
(defn listener:list
  ([{:keys [runtime]}]
   (collection/map-vals keys (:listeners @runtime))))
link
(listener:list |client|) => {:test [:id]}

listener:remove ^

[type {:keys [runtime], :as redis} id]
Added 3.0

removes a listener from the client

v 3.0
(defn listener:remove
  ([type {:keys [runtime] :as redis} id]
   (when-let [listener (-> (:listeners @runtime)
                           (get-in [type id]))]

     (listener:teardown listener)
     (swap! runtime update :listeners collection/dissoc-nested [type id])
     listener)))
link
(with-redefs [listener:teardown (constantly nil)] (swap! (:runtime |client|) assoc-in [:listeners :test :id] {}) (listener:remove :test |client| :id)) => {}

listener:teardown ^

[{:keys [type connection], :as listener}]
Added 3.0

tears down the listener

v 3.0
(defn listener:teardown
  ([{:keys [type connection] :as listener}]
   (let [{:keys [id args pool thread raw]} connection
         action (action:get type)
         _ (wire/write raw (into [(action :unsubscribe) args]))
         _ (future/future:cancel thread)
         _ (cc/pool:release pool id true)]
     id)))
link
(with-redefs [action:get (constantly {:unsubscribe "UNSUB"}) wire/write (constantly nil) future/future:cancel (constantly nil) cc/pool:release (constantly nil)] (listener:teardown {:type :test :connection {:id :id :args [] :pool :pool :thread :thread :raw :raw}})) => :id

listener? ^

[obj]
Added 3.0

checks that object is a listener

v 3.0
(defn listener?
  ([obj]
   (instance? Listener obj)))
link
(listener? (map->Listener {:type :test :id :id :namespace "ns" :handler identity :connection {}})) => true

map->Listener ^

[m__7997__auto__]

NONE
(impl/defimpl Listener [type id namespace handler connection]
  :string listener-string)
link

notify ^

[redis id pattern handler]
Added 3.0

notifications for a given client

v 3.0
(defn notify
  ([redis id pattern handler]
   (listener:add :notify redis id pattern handler {:pattern pattern})))
link
(with-redefs [listener:add (constantly :added)] (notify |client| :id "*" identity)) => :added

notify:args ^

[namespace inputs]
Added 3.0

produces the notify args

v 3.0
(defn notify:args
  ([namespace inputs]
   (mapv #(str "__keyspace@*:" (common/make-key namespace %))
         (collection/seqify inputs))))
link
(notify:args "test" ["input"]) => ["__keyspace@*:test:input"]

notify:wrap ^

[handler {:keys [namespace], :as redis}]
Added 3.0

wrapper for the notify delivery function

v 3.0
(defn notify:wrap
  ([handler {:keys [namespace] :as redis}]
   (fn [msg]
     (let [[type _ ^String path cmd :as msg] (wire/coerce msg :string)]
       (cond (= type "pmessage")
             (let [i   (.indexOf path ":")
                   key (subs path (inc i))]
               (env/explode
                (handler redis (common/unmake-key namespace key)))))))))
link
((notify:wrap (fn [redis key] key) {:namespace "test"}) [(.getBytes "pmessage") (.getBytes "pattern") (.getBytes "__keyspace@*:test:input") (.getBytes "set")]) => "input"

psubscribe ^

[redis id pattern handler]
Added 3.0

subscribes to a pattern on the cache

v 3.0
(defn psubscribe
  ([redis id pattern handler]
   (listener:add :psubscribe redis id pattern handler)))
link
(with-redefs [listener:add (constantly :added)] (psubscribe |client| :id ["*"] identity)) => :added

psubscribe:wrap ^

[handler {:keys [format], :as redis}]
Added 3.0

wrapper for the psubscribe delivery function

v 3.0
(defn psubscribe:wrap
  ([handler {:keys [format] :as redis}]
   (fn [msg]
     (let [[type pattern channel data] (seq msg)]
       (if (= (wire/coerce type :string) "pmessage")
         (handler (wire/coerce channel :string)
                  redis
                  nil
                  (wire/coerce data format)))))))
link
((psubscribe:wrap (fn [ch redis _ msg] msg) {:format :edn}) [(.getBytes "pmessage") (.getBytes "pattern") (.getBytes "channel") (.getBytes "{:a 1}")]) => {:a 1}

punsubscribe ^

[redis id]
Added 3.0

unsubscribes from the pattern

v 3.0
(defn punsubscribe
  ([redis id]
   (listener:remove :psubscribe redis id)))
link
(with-redefs [listener:remove (constantly :removed)] (punsubscribe |client| :id)) => :removed

start:events-redis ^

[redis events]
Added 3.0

creates action for `:events`

v 3.0
(defn start:events-redis
  ([redis events]
   (config:set redis events)
   redis))
link
(with-redefs [config:set (constantly nil)] (start:events-redis |client| #{})) => map?

start:notify-redis ^

[redis listeners]
Added 3.0

creates action for `:notify`

v 3.0
(defn start:notify-redis
  ([redis listeners]
   (doseq [[id {:keys [pattern handler]}] listeners]
     (notify redis id pattern handler))
   redis))
link
(with-redefs [notify (constantly nil)] (start:notify-redis |client| {:id {:pattern "*" :handler identity}})) => map?

stop:notify-redis ^

[redis listeners]
Added 3.0

stop action for `:notify` field

v 3.0
(defn stop:notify-redis
  ([redis listeners]
   (doseq [[id _] listeners]
     (unnotify redis id))
   redis))
link
(with-redefs [unnotify (constantly nil)] (stop:notify-redis |client| {:id {}})) => map?

subscribe ^

[redis id channels handler]
Added 3.0

subscribes to a channel on the cache

v 3.0
(defn subscribe
  ([redis id channels handler]
   (listener:add :subscribe redis id channels handler)))
link
(with-redefs [listener:add (constantly :added)] (subscribe |client| :id ["ch"] identity)) => :added

subscribe:wrap ^

[handler {:keys [format], :as redis}]
Added 3.0

wrapper for the subscribe delivery function

v 3.0
(defn subscribe:wrap
  ([handler {:keys [format] :as redis}]
   (fn [msg]
     (let [[type channel data] (seq msg)]
       (if (= (wire/coerce type :string) "message")
         (handler (wire/coerce channel :string)
                  redis
                  nil
                  (wire/coerce data format)))))))
link
((subscribe:wrap (fn [ch redis _ msg] msg) {:format :edn}) [(.getBytes "message") (.getBytes "channel") (.getBytes "{:a 1}")]) => {:a 1}

unnotify ^

[redis id]
Added 3.0

removes notifications for a given client

v 3.0
(defn unnotify
  ([redis id]
   (listener:remove :notify redis id)))
link
(with-redefs [listener:remove (constantly :removed)] (unnotify |client| :id)) => :removed

unsubscribe ^

[redis id]
Added 3.0

unsubscribes from a channel

v 3.0
(defn unsubscribe
  ([redis id]
   (listener:remove :subscribe redis id)))
link
(with-redefs [listener:remove (constantly :removed)] (unsubscribe |client| :id)) => :removed


+scripting+ ^

NONE
(def +scripting+
  (gen/select-commands {:group [:scripting]}))
link


+additional+ ^

NONE
(def +additional+
  (gen/select-commands {:group [:pubsub :scripting :hyperloglog :transactions]
                    :custom {:pfadd      {:arguments {1 {:type :data}}}
                             :publish    {:arguments {1 {:type :data}}}
                             :psubscribe {:arguments [{:type :key :sym 'patterns}]}
                             :subscribe  {:arguments [{:type :key}]}}}))
link

+all+ ^

NONE
(def +all+ (concat +management+
                   +generic+
                   +string+
                   +data+
                   +additional+))
link

+data+ ^

NONE
(def +data+
  (gen/select-commands {:group [:list :hash :set]
                    :custom  {:lpos    {:arguments {1 {:type :data}}}
                              :lpush   {:arguments {1 {:type :data}}}
                              :lpushx  {:arguments {1 {:type :data}}}
                              :lrem    {:arguments {2 {:type :data}}}
                              :lset    {:arguments {2 {:type :data}}}
                              :rpush   {:arguments {1 {:type :data}}}
                              :rpushx  {:arguments {1 {:type :data}}}

                              :hgetall {:return `common/return:kv-hash}
                              :hkeys   {:return `common/return:string}
                              :hmset   {:arguments {1 {:sym 'args :process `common/process:kv-hash}}}
                              :hsetnx  {:arguments {2 {:type :data}}}

                              :sadd      {:arguments {1 {:type :data}}}
                              :srem      {:arguments {1 {:type :data}}}
                              :sismember {:arguments {1 {:type :data}}}
                              :smove     {:arguments {2 {:type :data}}}}
                    :replace {:hset  {:id :hset
                                      :return :ack
                                      :arguments [{:type :key}
                                                  {:type :string :name "field"}
                                                  {:type :data}]}}}))
link

+generic+ ^

NONE
(def +generic+
  (gen/select-commands {:group :generic
                    :exclude [:sort :migrate]
                    :custom  {:keys {:arguments [{:name "pattern", :type :key}]
                                     :return `common/return:keys}}}))
link

+management+ ^

NONE
(def +management+
  (gen/select-commands {:group [:server :cluster :connection]
                    :exclude [:hello]
                    :custom  {:acl-log      {:arguments [{:name "count"}]}
                              :shutdown     {:arguments [{:name "mode"}]}}}))
link

+string+ ^

NONE
(def +string+
  (gen/select-commands {:group :string
                    :exclude [:bitcount :bitfield]
                    :custom  {:append {:arguments {1 {:type :data}}}
                              :getset {:arguments {1 {:type :data}}}
                              :psetex {:arguments {2 {:type :data}}}
                              :setex  {:arguments {2 {:type :data}}}
                              :setnx  {:arguments {1 {:type :data}}}
                              :msetnx {:arguments [{:sym 'args :process `common/process:kv}]}
                              :mset   {:arguments [{:sym 'args :process `common/process:kv}]}}
                    :replace {:set  {:id :set
                                     :return :ack
                                     :arguments [{:type :key}
                                                 {:type :data}
                                                 {:type :optional
                                                  :sym 'optional
                                                  :display '{:keys [expiry unit mode] :as optional}
                                                  :process `optional:set}]}}}))
link

optional:set ^

[{:keys [expiry unit mode]} _]
Added 4.0

optional parameters for `set` command

v 4.0
(defn optional:set
  ([{:keys [expiry unit mode]} _]
   (cond-> []
     mode   (conj (case mode
                    :not-exist "NX"
                    :exists "XX"))
     expiry (concat (cond (= expiry :keep)
                          ["KEEPTTL"]

                          (= unit :ms)
                          ["PX" expiry]

                          :else
                          ["EX" expiry]))
     :then vec)))
link
(optional:set {:expiry 10 :unit :ms :mode :exists} nil) => ["XX" "PX" 10]