├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pom.xml ├── project.clj ├── redis ├── docker-compose.yml └── redis-cli.sh ├── src └── redis_atom │ ├── RedisAtom.clj │ ├── core.clj │ └── redis.clj └── test └── redis_atom ├── RedisAtom_test.clj └── core_test.clj /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | tab_width = unset 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .vscode/ 11 | .lsp/ 12 | .settings/ 13 | .project 14 | .classpath -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.1.1 4 | - Remove clojure.core.async dependency 5 | - Minor fixes 6 | 7 | ## v1.1.0 8 | - New redis-atom should not overwrite existing key-value pair on backend. 9 | - Added no-value redis-atom arity. 10 | 11 | ## v1.0.0 12 | Support all clojure atom functionality -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexander Fertman (@sfertman) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis-atom 2 | Share clojure atoms between services via redis with one line of code. 3 | 4 | [![Clojars Project](https://img.shields.io/clojars/v/redis-atom.svg)](https://clojars.org/redis-atom) 5 | 6 | ## Using 7 | Define redis connection spec like with [`taoensso.carmine`](https://github.com/ptaoussanis/carmine). Define an atom by passing in the connetion spec, a key that will point to the atom value on Redis and the atom value. Note that if the input key already exists on redis backend then the input value will *not* be assigned to it. Everything else is exactly the same as with Clojure atoms. 8 | 9 | ```clojure 10 | (require '[redis-atom.core :refer [redis-atom]]) 11 | 12 | (def conn {:pool {} :spec {:uri "redis://localhost:6379"}}) 13 | 14 | (def a (redis-atom conn :redis-key {:my-data "42" :more-data 43})) 15 | 16 | a ; => #object[redis_atom.core.RedisAtom 0x471a378 {:status :ready, :val {:my-data "42", :more-data 43}}] 17 | @a ; => {:my-data "42" :more-data 43} 18 | 19 | (def b (redis-atom conn :redis-key 42)) 20 | @b ; => {:my-data "42" :more-data 43} 21 | 22 | (reset! a 42) ; => 42 23 | @b ; => 42 24 | 25 | (reset-vals! a 43) ; => [42 43] 26 | (swap! a inc) ; => 44 27 | (swap-vals! a inc) ; => [44 45] 28 | ``` 29 | 30 | ## Testing 31 | Running the test suite requires redis backend service which can be easily created with [docker-compose](https://docs.docker.com/compose/install/). 32 | To start a local backend: 33 | ```shell 34 | $ cd redis 35 | $ docker-compose up -d 36 | ``` 37 | This will start a redis server on `6379` and a redis-commander on `8081`. If you are a fan of redis-cli, run `redis-cli.sh` script in the same dir for the console. 38 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | redis-atom 4 | redis-atom 5 | jar 6 | 1.1.1 7 | redis-atom 8 | Share clojure atoms between services via redis with one line of code 9 | 10 | 11 | MIT 12 | https://opensource.org/licenses/mit-license.php 13 | 14 | 15 | 16 | https://github.com/sfertman/redis-atom 17 | scm:git:git://github.com/sfertman/redis-atom.git 18 | scm:git:ssh://git@github.com/sfertman/redis-atom.git 19 | c901e04ebbad6010d2cb899a6c82a01e9246e3af 20 | 21 | 22 | src 23 | test 24 | 25 | 26 | resources 27 | 28 | 29 | 30 | 31 | resources 32 | 33 | 34 | target 35 | target/classes 36 | 37 | 38 | 39 | 40 | central 41 | https://repo1.maven.org/maven2/ 42 | 43 | false 44 | 45 | 46 | true 47 | 48 | 49 | 50 | clojars 51 | https://repo.clojars.org/ 52 | 53 | true 54 | 55 | 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | com.taoensso 66 | carmine 67 | 2.19.1 68 | 69 | 70 | org.clojure 71 | clojure 72 | 1.10.0 73 | 74 | 75 | 76 | 77 | 81 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject redis-atom "1.1.1" 2 | :description "Share clojure atoms between services via redis with one line of code" 3 | :license { 4 | :name "MIT" 5 | :url "https://opensource.org/licenses/mit-license.php"} 6 | :dependencies [ 7 | [com.taoensso/carmine "2.19.1"] 8 | [org.clojure/clojure "1.10.0"]] 9 | :repl-options {:init-ns redis-atom.core} 10 | :aot [redis-atom.RedisAtom]) 11 | -------------------------------------------------------------------------------- /redis/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | redis_backend: 5 | container_name: redis_backend 6 | image: redis:alpine 7 | ports: 8 | - 6379:6379 9 | networks: 10 | - redis_atom_net 11 | redis_cmdr: 12 | container_name: redis_cmdr 13 | environment: 14 | REDIS_HOST: redis_backend 15 | image: rediscommander/redis-commander:latest 16 | networks: 17 | - redis_atom_net 18 | ports: 19 | - 8081:8081 20 | 21 | networks: 22 | redis_atom_net: 23 | name: redis_atom_net -------------------------------------------------------------------------------- /redis/redis-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker-compose exec redis_backend redis-cli -------------------------------------------------------------------------------- /src/redis_atom/RedisAtom.clj: -------------------------------------------------------------------------------- 1 | (ns redis-atom.RedisAtom 2 | (:require [redis-atom.redis :as r]) 3 | (:gen-class 4 | :name RedisAtom 5 | :extends clojure.lang.ARef 6 | :implements [clojure.lang.IDeref clojure.lang.IAtom2] 7 | :state state 8 | :init init 9 | :constructors { 10 | [clojure.lang.PersistentArrayMap clojure.lang.Keyword] [] 11 | [clojure.lang.PersistentArrayMap clojure.lang.Keyword clojure.lang.IPersistentMap] [clojure.lang.IPersistentMap]})) 12 | 13 | (defn -init 14 | ([conn k] [[] {:conn conn :k k}]) 15 | ([conn k mta] [[mta] {:conn conn :k k}])) 16 | 17 | (defn- validate* 18 | "This is a clojure re-implementation of clojure.lang.ARef/validate because 19 | cannot be accessed by subclasses Needed to invoke when changing atom state" 20 | [^clojure.lang.IFn vf val] 21 | (try 22 | (if (and (some? vf) (not (vf val))) 23 | (throw (IllegalStateException. "Invalid reference state"))) 24 | (catch RuntimeException re 25 | (throw re)) 26 | (catch Exception e 27 | (throw (IllegalStateException. "Invalid reference state" e))))) 28 | 29 | (defn -deref [this] 30 | (r/deref* (:conn (.state this)) (:k (.state this)))) 31 | 32 | (defn -reset [this newval] 33 | (validate* (.getValidator this) newval) 34 | (let [oldval (.deref this)] 35 | (r/reset* (:conn (.state this)) (:k (.state this)) newval) 36 | (.notifyWatches this oldval newval) 37 | newval)) 38 | ;; ^^ Note: this looks dubious but this is the way clojure atom works at the moment. Seems like there's no point ensuring atomic tx here since there are explicit tools for that, namely swap!. 39 | 40 | (defn -compareAndSet [this oldval newval] 41 | (validate* (.getValidator this) newval) 42 | (let [ret (r/compare-and-set* (:conn (.state this)) (:k (.state this)) oldval newval)] 43 | (when ret 44 | (.notifyWatches this oldval newval)) 45 | ret)) 46 | 47 | (defn -resetVals [this newval] 48 | (loop [oldval (.deref this)] 49 | (if (.compareAndSet this oldval newval) 50 | [oldval newval] 51 | (recur (.deref this))))) 52 | 53 | (defn- swap* 54 | [this f & args] 55 | (loop [oldval (.deref this)] 56 | (let [newval (apply f oldval args)] 57 | (if (.compareAndSet this oldval newval) 58 | newval 59 | (recur (.deref this)))))) 60 | 61 | (defn -swap-IFn 62 | [this f] (swap* this f)) 63 | (defn -swap-IFn-Object 64 | [this f x] (swap* this f x)) 65 | (defn -swap-IFn-Object-Object 66 | [this f x y] (swap* this f x y)) 67 | (defn -swap-IFn-Object-Object-ISeq 68 | [this f x y args] (apply swap* this f x y args)) 69 | 70 | (defn- swap-vals* 71 | [this f & args] 72 | (loop [oldval (.deref this)] 73 | (let [newval (apply f oldval args)] 74 | (if (.compareAndSet this oldval newval) 75 | [oldval newval] 76 | (recur (.deref this)))))) 77 | 78 | (defn -swapVals-IFn 79 | [this f] (swap-vals* this f)) 80 | (defn -swapVals-IFn-Object 81 | [this f x] (swap-vals* this f x)) 82 | (defn -swapVals-IFn-Object-Object 83 | [this f x y] (swap-vals* this f x y)) 84 | (defn -swapVals-IFn-Object-Object-ISeq 85 | [this f x y args] (apply swap-vals* this f x y args)) -------------------------------------------------------------------------------- /src/redis_atom/core.clj: -------------------------------------------------------------------------------- 1 | (ns redis-atom.core 2 | (:require 3 | [redis-atom.RedisAtom] 4 | [redis-atom.redis :refer [setnx*]])) 5 | 6 | (defn redis-atom 7 | ([conn k] (RedisAtom. conn k)) 8 | ([conn k val] (let [a (redis-atom conn k)] (setnx* conn k val) a)) 9 | ([conn k val & {mta :meta v-tor :validator}] 10 | (let [a (redis-atom conn k val)] 11 | (when mta (.resetMeta a mta)) 12 | (when v-tor (.setValidator a v-tor)) 13 | a))) -------------------------------------------------------------------------------- /src/redis_atom/redis.clj: -------------------------------------------------------------------------------- 1 | (ns redis-atom.redis 2 | (:require [taoensso.carmine :as r])) 3 | 4 | (defn deref* [conn k] (:data (r/wcar conn (r/get k)))) 5 | 6 | (defn reset* [conn k newval] (r/wcar conn (r/set k {:data newval}))) 7 | 8 | (defn setnx* [conn k newval] (r/wcar conn (r/setnx k {:data newval}))) 9 | 10 | (defn compare-and-set* [conn k oldval newval] 11 | (r/wcar conn (r/watch k)) 12 | (if (not= oldval (deref* conn k)) 13 | (do (r/wcar conn (r/unwatch)) 14 | false) 15 | (some? (r/wcar conn 16 | (r/multi) 17 | (r/set k {:data newval}) 18 | (r/exec))))) -------------------------------------------------------------------------------- /test/redis_atom/RedisAtom_test.clj: -------------------------------------------------------------------------------- 1 | (ns redis-atom.RedisAtom-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [redis-atom.RedisAtom])) 5 | 6 | (def conn {:a 1 :b 2 :c {:d 4}}) 7 | 8 | (deftest test-init 9 | (let [a (RedisAtom. conn :test-init)] 10 | (is (= {:conn conn :k :test-init} (.state a)))) 11 | (let [a (RedisAtom. conn :test-init-with-meta {:hello "world"})] 12 | (is (= {:conn conn :k :test-init-with-meta} (.state a))) 13 | (is (= {:hello "world"} (meta a))))) 14 | -------------------------------------------------------------------------------- /test/redis_atom/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns redis-atom.core-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [redis-atom.core :refer [redis-atom]] 5 | [taoensso.carmine :as redis]) 6 | (:import 7 | java.lang.IllegalStateException)) 8 | 9 | (def conn {:pool {} :spec {:uri "redis://localhost:6379"}}) 10 | 11 | (defmacro wcar* [& body] `(redis/wcar conn ~@body)) 12 | 13 | (wcar* (redis/flushall)) 14 | 15 | (deftest test-create 16 | (testing "create with conn & k *without* existing key on backend" 17 | (let [a (redis-atom conn :test-create-1) 18 | state-a (.state a)] 19 | (is (= conn (:conn state-a))) 20 | (is (= :test-create-1 (:k state-a))) 21 | (is (nil? @a)))) 22 | (testing "create with conn & k *with* existing key on backend" 23 | (let [_ (redis-atom conn :test-create-2 42) 24 | a (redis-atom conn :test-create-2) 25 | state-a (.state a)] 26 | (is (= conn (:conn state-a))) 27 | (is (= :test-create-2 (:k state-a))) 28 | (is (= 42 @a)))) 29 | (testing "create with conn, k & val *without* existing key on backend" 30 | (let [a (redis-atom conn :test-create-3 42) 31 | state-a (.state a)] 32 | (is (= conn (:conn state-a))) 33 | (is (= :test-create-3 (:k state-a))) 34 | (is (= 42 @a)))) 35 | (testing "create with conn, k & val *with* existing key on backend" 36 | (let [_ (redis-atom conn :test-create-4 42) 37 | a (redis-atom conn :test-create-4 43) 38 | state-a (.state a)] 39 | (is (= conn (:conn state-a))) 40 | (is (= :test-create-4 (:k state-a))) 41 | (is (= 42 @a))))) 42 | 43 | (deftest test-deref 44 | (let [a (redis-atom conn :test-deref 42)] 45 | (is (= 42 @a)))) 46 | 47 | (deftest test-reset 48 | (let [a (redis-atom conn :test-reset 42)] 49 | (is (= 42 @a)) 50 | (is (= 44 (reset! a 44))) 51 | (is (= 44 @a)))) 52 | 53 | (deftest test-reset-vals 54 | (let [a (redis-atom conn :test-reset-val 42)] 55 | (is (= 42 @a)) 56 | (is (= [42 43] (reset-vals! a 43))) 57 | (is (= 43 @a)) 58 | (is (= [43 44] (reset-vals! a 44))) 59 | (is (= 44 @a)) 60 | (is (= [44 45] (reset-vals! a 45))) 61 | (is (= 45 @a)) 62 | (is (= [45 "abc"] (reset-vals! a "abc"))) 63 | (is (= "abc" @a)))) 64 | 65 | (deftest test-compare-and-set 66 | (let [a (redis-atom conn :test-compare-and-set 42)] 67 | (is (= 42 @a)) 68 | (is (false? (compare-and-set! a 57 44))) 69 | (is (= 42 @a)) 70 | (is (true? (compare-and-set! a 42 44))) 71 | (is (= 44 @a)))) 72 | 73 | (defn wait-and-inc 74 | [t-ms x] 75 | (Thread/sleep t-ms) 76 | (let [xpp (inc x)] 77 | (prn (str "wait-and-inc waited for " t-ms " [ms]: " x " + 1 = " xpp )) 78 | xpp)) 79 | 80 | (deftest test-swap-arity 81 | (let [a (redis-atom conn :test-swap-arity 42)] 82 | (is (= 43 (swap! a inc))) 83 | (is (= 44 (swap! a + 1))) 84 | (is (= 46 (swap! a + 1 1))) 85 | (is (= 49 (swap! a + 1 1 1))) 86 | (is (= 53 (swap! a + 1 1 1 1))))) 87 | 88 | (deftest test-swap-vals-arity 89 | (let [a (redis-atom conn :test-swap-vals-arity 42)] 90 | (is (= [42 43] (swap-vals! a inc))) 91 | (is (= [43 44] (swap-vals! a + 1))) 92 | (is (= [44 46] (swap-vals! a + 1 1))) 93 | (is (= [46 49] (swap-vals! a + 1 1 1))) 94 | (is (= [49 53] (swap-vals! a + 1 1 1 1))))) 95 | 96 | (deftest test-swap-locking 97 | (let [a (redis-atom conn :test-swap-locking 42) 98 | b (redis-atom conn :test-swap-locking)] 99 | (future 100 | (is (= 44 (swap! a (partial wait-and-inc 100))))) 101 | (future 102 | (is (= 43 (swap! b (partial wait-and-inc 50))))) 103 | (Thread/sleep 250))) 104 | 105 | (deftest test-swap-vals-locking 106 | (let [a (redis-atom conn :test-swap-vals-locking 42) 107 | b (redis-atom conn :test-swap-vals-locking)] 108 | (future 109 | (is (= [43 44] (swap-vals! a (partial wait-and-inc 100))))) 110 | (future 111 | (is (= [42 43] (swap-vals! b (partial wait-and-inc 50))))) 112 | (Thread/sleep 250))) 113 | 114 | (deftest test-watches 115 | (let [a (redis-atom conn :test-watches 42) 116 | watcher-atom (atom nil)] 117 | (add-watch a :watcher (fn [& args] (reset! watcher-atom args))) 118 | (reset! a 43) 119 | (is (= 43 @a)) 120 | (is (= @watcher-atom [:watcher a 42 43])) 121 | (remove-watch a :watcher) 122 | (reset! a 44) 123 | (is (= 44 @a)) 124 | (is (= @watcher-atom [:watcher a 42 43])))) 125 | 126 | (defmacro try-catch-invalid-state [form] 127 | `(try ~form 128 | (is (= 0 1)) 129 | (catch IllegalStateException e# 130 | (is (= "Invalid reference state") (.getMessage e#))))) 131 | 132 | (deftest test-validator 133 | (let [a (redis-atom conn :test-valdator 42 :validator (fn [newval] (< newval 43)))] 134 | (is (= 42 @a)) 135 | (try-catch-invalid-state (reset! a 43)) 136 | (try-catch-invalid-state (reset-vals! a 43)) 137 | (try-catch-invalid-state (swap! a inc)) 138 | (try-catch-invalid-state (swap! a + 1)) 139 | (try-catch-invalid-state (swap! a + 1 1)) 140 | (try-catch-invalid-state (swap! a + 1 1 1)) 141 | (try-catch-invalid-state (swap! a + 1 1 1 1)) 142 | (try-catch-invalid-state (swap-vals! a inc)) 143 | (try-catch-invalid-state (swap-vals! a + 1)) 144 | (try-catch-invalid-state (swap-vals! a + 1 1)) 145 | (try-catch-invalid-state (swap-vals! a + 1 1 1)) 146 | (try-catch-invalid-state (swap-vals! a + 1 1 1 1)) 147 | (try-catch-invalid-state (compare-and-set! a 42 43)) 148 | (try-catch-invalid-state (compare-and-set! a 43 44)))) 149 | 150 | (deftest test-meta 151 | (let [a (redis-atom conn :test-meta 42 :meta {:hello "meta"})] 152 | (is (= 42 @a)) 153 | (is (= {:hello "meta"} (meta a))))) --------------------------------------------------------------------------------