├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── project.clj ├── src └── quanta │ ├── core.clj │ ├── database.clj │ ├── handler.clj │ ├── http.clj │ ├── message.clj │ ├── node.clj │ ├── udp.clj │ └── util.clj └── test └── quanta ├── test_database.clj ├── test_handler.clj ├── test_message.clj ├── test_node.clj └── test_util.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | /target 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *jar 8 | /lib/ 9 | /classes/ 10 | /target/ 11 | /checkouts/ 12 | .lein-deps-sum 13 | .lein-repl-history 14 | .lein-plugins/ 15 | .lein-failures 16 | *.jar 17 | *.class 18 | /.lein-* 19 | /.nrepl-port 20 | !.travis.yml 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quanta 2 | 3 | *A Clojure port of [Vignette](https://github.com/avibryant/vignette).* 4 | 5 | [![Build Status](https://travis-ci.org/maxcountryman/quanta.svg?branch=master)](https://travis-ci.org/maxcountryman/quanta) 6 | 7 | Quanta is a distributed, highly-available, eventually-consistent sketch 8 | database. 9 | 10 | Quanta is a specialized key-value store, where keys are strings and values are 11 | sparse integer vectors. Values can only be modified with element-wise max. 12 | This limitation ensures a [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) 13 | property--the `max` operation is associative, commutative, and idempotent. That 14 | means that updates can be performed in any order, in any combination, and as 15 | many times as we want without inconsistency. 16 | 17 | While this might seem like a limitation which precludes usefulness, a 18 | surprising number of interesting applications can be implemented over these 19 | constraints: bloom filters, vector clocks, and hyperloglog are all examples of 20 | applications that can be easily implemented using Quanta. 21 | 22 | ## Status 23 | 24 | Under development and totally unsuitable for production use! 25 | 26 | I'm building this as a way to learn about distributed systems, so you should 27 | not necessarily expect this to ever be fit for use in a realworld system. 28 | 29 | ## Installation 30 | 31 | Currently, Quanta nodes are backed by LevelDB. Ensure LevelDB is installed on 32 | your platform before running. 33 | 34 | Quanta is available on Clojars. 35 | 36 | [![Clojars Project](http://clojars.org/quanta/latest-version.svg)](http://clojars.org/quanta) 37 | 38 | ## Usage 39 | 40 | To run a node with two known peers: 41 | 42 | ```sh 43 | $ lein run --node-addr 'localhost:3000' --peers 'localhost:3001 localhost:3002' 44 | ``` 45 | 46 | This will set up a node listening on localhost:3000. It will expect to find 47 | peer nodes on localhost:3001 and localhost:3002. As the node starts it will 48 | bootstrap itself by requesting a peerlist from the given peers. 49 | 50 | ### HTTP Interface 51 | 52 | Each node is accessible via a simple REST-like HTTP interface. 53 | 54 | To set a key in the node, a PUT request may be used. The body is a JSON 55 | representation of the vector to be stored or updated. Using curl as an example 56 | client: 57 | 58 | ```sh 59 | $ curl -XPUT localhost:3000 \ 60 | -d '{"k": "foo", "v": {"0": 42}}' \ 61 | -H 'content-type: application/json' 62 | ``` 63 | 64 | This associates the key "foo" with the vector `{0 42}` in the node. If a vector 65 | already exists with the key "foo" then its indexes which match the provided 66 | indexes will be updated via element-wise max. Any missing indexes or indexes 67 | with larger values than provided will be return in the response as a 68 | JSON-encoded vector. 69 | 70 | Retrieving a key is a special case of setting a key. Sending an empty vector 71 | to a node will cause the node to return the vector in its entirety: 72 | 73 | ```sh 74 | $ curl -XPUT localhost:3000 \ 75 | -d '{"k": "foo", "v": {}}' \ 76 | -H 'content-type: application/json' 77 | ``` 78 | 79 | The response will contain the complete vector the node is currently aware of. 80 | This will look something like: 81 | 82 | ```json 83 | [ 84 | { 85 | "k": "foo", 86 | "v": { 87 | "0": 42 88 | }, 89 | "ttl": 0 90 | } 91 | ] 92 | ``` 93 | 94 | Finally the same method can be used for searches and aggregates. For example, 95 | we can retrieve a list of vectors for all keys prefixed with "foo": 96 | 97 | ```sh 98 | $ curl -XPUT localhost:3000 \ 99 | -d '{"k": "foo*", "v": {}}' \ 100 | -H 'content-type: application/json' 101 | ``` 102 | 103 | ## Design 104 | 105 | See [Vignette](https://github.com/avibryant/vignette/blob/master/README.md). 106 | 107 | ## License 108 | 109 | Copyright © 2014 Max Countryman 110 | 111 | Distributed under the Eclipse Public License either version 1.0 or (at 112 | your option) any later version. 113 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject quanta "0.1.0-SNAPSHOT" 2 | :description "Distributed sparse integer vectors." 3 | :url "https://github.com/maxcountryman/quanta" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[clout "2.1.0"] 7 | [com.cognitect/transit-clj "0.8.247"] 8 | [factual/clj-leveldb "0.1.1"] 9 | [org.clojure/clojure "1.6.0"] 10 | [org.clojure/tools.cli "0.3.1"] 11 | [org.clojure/tools.logging "0.3.0"] 12 | [ring/ring-jetty-adapter "1.3.2"] 13 | [ring/ring-json "0.3.1"]] 14 | :global-vars {*warn-on-reflection* true} 15 | :jvm-opts ^:replace ["-server"] 16 | :profiles {:uberjar {:aot :all}} 17 | :main quanta.core) 18 | -------------------------------------------------------------------------------- /src/quanta/core.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.core 2 | "Quanta is a distributed CRDT of keys and values. 3 | 4 | Keys may be associated with values, which are sparse vectors of integers. 5 | These vectors may be updated only via element-wise max. This constraint 6 | yields a CRDT property because the max operation is idempotent, commutative, 7 | and associative. 8 | 9 | This quality makes Quanta useful as a kind of sketch database. For example, 10 | HyperLogLog can be implemented with the above constraints. Other possible 11 | uses include bloom filters, vector clocks, and min-hashes." 12 | (:require [clojure.string :as string] 13 | [clojure.tools.cli :refer [parse-opts]] 14 | [clojure.tools.logging :as log] 15 | [quanta.database :as database] 16 | [quanta.handler :as handler] 17 | [quanta.node :as node] 18 | [quanta.udp :as udp] 19 | [quanta.util :as util]) 20 | (:gen-class)) 21 | 22 | (def specs 23 | [["-a" 24 | "--node-addr NODEADDR" 25 | "Address to bind UDP socket to" 26 | :assoc-fn (fn [m k v] 27 | (let [[host port] (util/parse-addr v)] 28 | ;; Increment the HTTP server port by 100. 29 | (-> m 30 | (update-in [:http-addr] (constantly 31 | (str host ":" (+ port 100)))) 32 | (assoc k v)))) 33 | :default "localhost:3000"] 34 | ["-H" 35 | "--http-addr HTTPADDR" 36 | "Address to bind HTTP server to" 37 | :default "localhost:3100"] 38 | ["-p" 39 | "--peers PEERS" 40 | "Addresses of known peers" 41 | :parse-fn #(string/split % #" ")] 42 | ["-h" 43 | "--help" 44 | "Print this help" 45 | :default false]]) 46 | 47 | (defn peer-map 48 | [peers] 49 | (into {} (map #(vector 50 | (str "n:" %) 51 | {0 (handler/timestamp)}) 52 | peers))) 53 | 54 | (defn -main 55 | [& args] 56 | (let [{:keys [summary] {:keys [help http-addr node-addr peers]} :options} 57 | (parse-opts args specs)] 58 | (when help 59 | (println summary)) 60 | 61 | (when-not help 62 | (let [peers (peer-map peers)] 63 | (log/info "Starting quanta node on" node-addr) 64 | (log/info "Starting HTTP server on" http-addr) 65 | (-> (node/new node-addr http-addr peers) node/start))))) 66 | -------------------------------------------------------------------------------- /src/quanta/database.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.database 2 | "Persistence layer abstractions." 3 | (:refer-clojure :exclude [get]) 4 | (:require [clj-leveldb :as level] 5 | [clojure.core :as core] 6 | [clojure.data :refer [diff]] 7 | [clojure.tools.logging :as log] 8 | [clojure.set :refer [difference union]] 9 | [clojure.string :as string] 10 | [quanta.message :as message]) 11 | (:import [clj_leveldb LevelDB] 12 | [java.io Closeable])) 13 | 14 | ;; 15 | ;; Trigram indexing. 16 | ;; 17 | 18 | ;; To achieve wildcard queries, a virtual trigram index is used. This index is 19 | ;; built by splitting keys into trigrams and storing those trigrams as keys. 20 | ;; These contain sets of keys in the primary store. For example, the key 21 | ;; "foobar" becomes ("foo" "oob" "oba" "bar"), each of these being a key in the 22 | ;; trigram index containing the set #{"foobar"}. 23 | 24 | (defn n-grams 25 | "Given n (gram size) and a string with which to make n-grams from, returns a 26 | lazy sequence of n-grams." 27 | [n s] 28 | (partition n 1 s)) 29 | 30 | (defn trigram-keys 31 | "Returns a lazy sequence of trigrams of the given key." 32 | [k] 33 | (map (partial apply str) (n-grams 3 k))) 34 | 35 | (defn query-trigrams 36 | "Queries the database by breaking the provided substring into trigrams and 37 | then retrieving those. After retrieval the provided regex pattern is applied 38 | to the result. 39 | 40 | In the event the provided substring is fewer than three characters (i.e. not 41 | a trigram), we iterate all the keys, keeping any that contain the substring. 42 | These are then filtered as above, using regex. 43 | 44 | Returns a list of matching keys. These keys reside in the primary database. 45 | Note that this implies the trigram index was populated in tandem with the 46 | primary." 47 | [db s re] 48 | (when-let [ks (if (> (count s) 2) 49 | ;; Find exact trigrams, retrieve their keys' sets. 50 | (reduce (fn [acc trigram] 51 | (if-let [ks (level/get db trigram)] 52 | (conj acc ks) 53 | acc)) 54 | () 55 | (trigram-keys s)) 56 | 57 | ;; Traverse trigrams, searching for any containing the 58 | ;; substring s, retrieve their keys' sets. 59 | (reduce (fn [acc [trigram ks]] 60 | (if (.contains ^String trigram s) 61 | (conj acc ks) 62 | acc)) 63 | () 64 | (level/iterator db)))] 65 | (->> ks 66 | (apply union) 67 | (filter #(re-matches re %))))) 68 | 69 | ;; 70 | ;; Store abstraction. 71 | ;; 72 | 73 | (defn k->re-s 74 | "Returns a tuple where the first element is a regex pattern and the second 75 | is a substring." 76 | [k] 77 | (vector (re-pattern (-> k 78 | (string/replace "." "[.]") 79 | (string/replace "*" ".*") 80 | (string/replace "%" ".*"))) 81 | (-> k 82 | (string/replace "%" "") 83 | (string/replace "*" "")))) 84 | 85 | (defprotocol Store 86 | (get [s k]) 87 | (match [s substring]) 88 | (put! [s k v])) 89 | 90 | (deftype MemoryStore [a] 91 | Store 92 | (get [_ k] 93 | (core/get @a k)) 94 | 95 | (match [_ substring] 96 | (let [[re s] (k->re-s substring)] 97 | (reduce-kv (fn [matches k v] 98 | (if (re-matches re k) 99 | (conj matches k) 100 | matches)) 101 | () 102 | @a))) 103 | 104 | (put! [_ k v] 105 | (swap! a assoc k v) 106 | v) 107 | 108 | clojure.lang.IDeref 109 | (deref [_] 110 | @a)) 111 | 112 | (defn memory-store 113 | "Given an atom, returns a new MemoryStore." 114 | [a] 115 | (MemoryStore. a)) 116 | 117 | (deftype LevelDBStore [^LevelDB db ^LevelDB tindex] 118 | Store 119 | (get [_ k] 120 | (level/get db k)) 121 | 122 | (match [_ substring] 123 | (let [[re s] (k->re-s substring)] 124 | (query-trigrams tindex s re))) 125 | 126 | (put! [_ k v] 127 | ;; As we insert a new key into the primary store, we also update the 128 | ;; trigram index store. 129 | (let [trigrams (trigram-keys k)] 130 | (doseq [trigram trigrams] 131 | (level/put tindex trigram (if-let [existing (level/get tindex trigram)] 132 | (union existing #{k}) 133 | #{k})))) 134 | (level/put db k v) 135 | v) 136 | 137 | Closeable 138 | (close [_] 139 | (.close db) 140 | (.close tindex))) 141 | 142 | (defn create-db 143 | "Given a path, returns a Closeable database object with preset key-decoder, 144 | val-encoder, val-decoder: byte-streams/to-string, message/encode, 145 | message/decode, respeactively." 146 | [path] 147 | (level/create-db path {:key-decoder byte-streams/to-string 148 | :val-encoder message/encode 149 | :val-decoder message/decode})) 150 | 151 | (defn leveldb-store 152 | "Given a primary path and trigram path, returns a new LevelDBStore." 153 | [ppath tpath] 154 | (LevelDBStore. (create-db ppath) (create-db tpath))) 155 | 156 | ;; 157 | ;; Query functions. 158 | ;; 159 | 160 | (defn put-with-merge! 161 | "Puts a value into the store but first checks to see if the key already 162 | exists. When it does, updates that existing value with the provided merge 163 | function." 164 | [store k v merge-fn] 165 | (put! store k (if-let [existing (get store k)] 166 | (merge-fn existing v) 167 | v))) 168 | 169 | (defn max-vector 170 | "Returns a map of only indices which either do not exist or are larger than 171 | provided values." 172 | [store k v] 173 | (when-let [existing (get store k)] 174 | (if (empty? v) 175 | existing 176 | (merge (reduce-kv (fn [updates i n] 177 | (if-let [m (core/get existing i)] 178 | (if (> m n) 179 | (assoc updates i m) 180 | updates))) 181 | {} 182 | v) 183 | (first (diff existing v)))))) 184 | 185 | (defn update-vector! 186 | "Updates the store where provided vector indices either do not exist or are 187 | larger than stored values. Returns a map of any updates." 188 | [store k v] 189 | (if-let [existing (get store k)] 190 | (reduce-kv (fn [updates i n] 191 | (let [m (core/get existing i 0)] 192 | (if (> n m) 193 | (do (put-with-merge! store k {i n} merge) 194 | (assoc updates i n)) 195 | updates))) 196 | {} 197 | v) 198 | (put! store k v))) 199 | -------------------------------------------------------------------------------- /src/quanta/handler.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.handler 2 | "Message handling logic." 3 | (:require [clojure.string :as string] 4 | [quanta.database :as database] 5 | [quanta.message :as message] 6 | [quanta.util :as util])) 7 | 8 | ;; 9 | ;; Message sending. 10 | ;; 11 | 12 | (defn timestamp [] (quot (System/currentTimeMillis) (* 1000 6))) 13 | 14 | (defn addr->peer-key [addr] (str "n:" addr)) 15 | 16 | (defn peer-key->addr [peer-key] (apply str (drop 2 peer-key))) 17 | 18 | (defn rand-peer 19 | "Returns a random peer address, excluding this node." 20 | [{:keys [node-addr peers]}] 21 | (some-> @peers 22 | (dissoc (addr->peer-key node-addr)) 23 | keys 24 | rand-nth 25 | peer-key->addr)) 26 | 27 | (defn send-heartbeat 28 | "Sends a heartbeat message to a peer." 29 | [socket peer addr] 30 | (->> (message/new (addr->peer-key addr) {0 (timestamp)} 0) 31 | (message/send socket peer))) 32 | 33 | (defn send-messages 34 | "Sends messages via the given socket to the given address. Messages are 35 | assumed to be a sequence of messages formatted via message/new." 36 | [socket addr messages] 37 | (doseq [message messages] 38 | (message/send socket addr message))) 39 | 40 | (defn send-responses 41 | "Sends forwards and responses, when they exist, to a random peer and the 42 | sender respectively. The first argument should be a node map and the second 43 | argument should be a message map containing a list of forwards and 44 | responses." 45 | [{:keys [socket] :as node} {:keys [addr forwards responses]}] 46 | ;; Send forward any new values and heartbeat, acknowledging the sender's 47 | ;; liveness to the cluster. 48 | ;; 49 | ;; TODO: Possibly this should forward to N + 1 / 2 peers, where N is the 50 | ;; number of known peers. 51 | (when (seq forwards) 52 | (when-let [peer (rand-peer node)] 53 | (send-heartbeat socket peer addr) 54 | (send-messages socket peer forwards))) 55 | 56 | ;; Send back any updated values. 57 | (when (seq responses) 58 | (send-messages socket addr responses))) 59 | 60 | ;; 61 | ;; Message handling. 62 | ;; 63 | 64 | (defn heartbeat? 65 | "Returns true if a key is formatted as a heartbeat otherwise false." 66 | [k] 67 | (.startsWith ^String k "n:")) 68 | 69 | (defn get-store 70 | "Returns either the peer list store or the key store, depending on message 71 | type." 72 | [node k] 73 | (if (heartbeat? k) 74 | (:peers node) 75 | (:store node))) 76 | 77 | (defn forward! 78 | "Returns a message intended to be forwarded onto a peer or if there are no 79 | relevant changes nil. This function may mutate the datastore!" 80 | [store k v ttl] 81 | (let [updates (database/update-vector! store k v)] 82 | (when (and (seq updates) (> ttl 0)) 83 | (message/new k updates (dec ttl))))) 84 | 85 | (defn response 86 | "Returns a message intended to be sent back to the sending peer or if there 87 | are no relevant changes nil." 88 | [store k v ttl] 89 | (when (> ttl 0) 90 | (let [updates (database/max-vector store k v)] 91 | (when (seq updates) 92 | (message/new k updates (dec ttl)))))) 93 | 94 | (defn message-type 95 | "Returns either :aggregate, :search, or :default." 96 | [_ {:keys [k]}] 97 | (condp #(.contains ^String %2 %1) k 98 | "*" :aggregate 99 | "%" :search 100 | :default)) 101 | 102 | (defn update-peer-list! 103 | [{:keys [peers] :as node} {:keys [addr] :as msg}] 104 | (when addr 105 | (database/update-vector! peers (addr->peer-key addr) {0 (timestamp)})) 106 | msg) 107 | 108 | (defmulti handle-message message-type) 109 | 110 | (defmethod handle-message :default 111 | [node {:keys [addr k v ttl]}] 112 | (let [store (get-store node k)] 113 | {:addr addr 114 | :forwards (keep #(forward! store % v ttl) [k]) 115 | :responses (keep #(response store % v ttl) [k])})) 116 | 117 | (defmethod handle-message :search 118 | [node {:keys [addr k v ttl]}] 119 | (let [store (get-store node k) 120 | ks (database/match store k) 121 | msgs (map #(-> (message/new % v ttl) 122 | (assoc :addr addr)) ks)] 123 | 124 | (apply (partial merge-with (fn [left right] 125 | (if (seq? left) 126 | (concat left right) 127 | left))) 128 | (map (partial handle-message node) msgs)))) 129 | 130 | (defmethod handle-message :aggregate 131 | [node {:keys [addr k v ttl]}] 132 | (let [store (get-store node k) 133 | agg (->> (database/match store k) 134 | (map #(database/get store %)) 135 | (remove empty?) 136 | (apply merge-with max))] 137 | 138 | ;; Ensure we store the aggregate of the local keys in the database prior to 139 | ;; processing the incoming message. 140 | (database/put! store k agg) 141 | 142 | {:addr addr 143 | :forwards (keep #(forward! store % v ttl) [k]) 144 | :responses (keep #(response store % v ttl) [k])})) 145 | -------------------------------------------------------------------------------- /src/quanta/http.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.http 2 | "HTTP interface." 3 | (:require [clout.core :refer [route-matches]] 4 | [ring.adapter.jetty :refer [run-jetty]] 5 | [ring.middleware.json :refer [wrap-json-body 6 | wrap-json-response]] 7 | [quanta.handler :as handler])) 8 | 9 | (defn json? 10 | [{:keys [content-type]}] 11 | (boolean 12 | (when content-type 13 | (re-find #"^application/(.+\+)?json" content-type)))) 14 | 15 | (defn ensure-json 16 | [request] 17 | (when-not (json? request) 18 | {:status 400 :body "Non-JSON Content-Type"})) 19 | 20 | (defn ensure-key 21 | [{{:strs [k]} :body}] 22 | (when-not k {:status 400 :body "Missing key"})) 23 | 24 | (defn ensure-value 25 | [{{:strs [v]} :body}] 26 | (when-not v {:status 400 :body "Missing value"})) 27 | 28 | ;; TODO: Stricter evaluation of vector, e.g. need to ensure indices are longs! 29 | (defn ensure-vector 30 | [{{:strs [v]} :body}] 31 | (when-not (map? v) 32 | {:status 400 :body "Malformed vector"})) 33 | 34 | (defn keys->longs 35 | "Converts the keys of a given map from strings to longs." 36 | [m] 37 | (into {} 38 | (for [[k v] m] 39 | [(java.lang.Long/parseLong k) v]))) 40 | 41 | (defmulti response :request-method) 42 | 43 | (defmethod response :put 44 | [{:keys [node] 45 | {:keys [socket node-addr]} :node 46 | {:strs [k v ttl]} :body :as request}] 47 | (let [some-errors (some-fn ensure-json 48 | ensure-key 49 | ensure-value 50 | ensure-vector)] 51 | (or (some-errors request) 52 | (let [msg {:k k :v (keys->longs v) :ttl (or ttl 1)} 53 | {:keys [forwards responses]} (handler/handle-message node msg)] 54 | (when (seq forwards) 55 | (when-let [peer (handler/rand-peer node)] 56 | (handler/send-messages socket peer forwards))) 57 | (if (every? #(= msg %) responses) 58 | {:status 201 :body responses} 59 | {:status 200 :body responses}))))) 60 | 61 | (defmethod response :default 62 | [_] 63 | {:status 405 :body "Method Not Allowed"}) 64 | 65 | ;; 66 | ;; HTTP Routes. 67 | ;; 68 | 69 | (defn wrap-routes 70 | "Middleware which provides various HTTP endpoints. In particular this exposes 71 | a health endpoint, a key retrieval endpoint, and a key setting endpoint. An 72 | HTTP client may be used to access these endpoints." 73 | [handler {:keys [peers] :as node}] 74 | (fn [request] 75 | (condp route-matches request 76 | "/health" {:status 200 77 | :body {:status :okay :peers @peers}} 78 | "/" (-> request 79 | (assoc :node node) 80 | response) 81 | (handler request)))) 82 | 83 | (defn new 84 | [node host port] 85 | (-> (constantly {:status 404 :body "Not Found"}) 86 | (wrap-routes node) 87 | (wrap-json-body {:bigdecimal? true}) 88 | (wrap-json-response {:pretty true}) 89 | (run-jetty {:host host :port port :join? false}))) 90 | -------------------------------------------------------------------------------- /src/quanta/message.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.message 2 | "Message abstractions." 3 | (:require [clojure.tools.logging :as log] 4 | [cognitect.transit :as transit] 5 | [quanta.udp :as udp] 6 | [quanta.util :as util]) 7 | (:refer-clojure :exclude [send]) 8 | (:import [java.io ByteArrayInputStream ByteArrayOutputStream InputStream] 9 | [java.net DatagramPacket InetSocketAddress])) 10 | 11 | (def ^{:const true} TRANSIT-FORMAT :msgpack) 12 | 13 | (defn encode 14 | "Writes message with MessagePack format into a ByteArrayOutputStream as 15 | transit data. Returns the output stream." 16 | [message] 17 | (let [out (ByteArrayOutputStream.) 18 | writer (transit/writer out TRANSIT-FORMAT)] 19 | (transit/write writer message) 20 | (.toByteArray out))) 21 | 22 | (defn decode 23 | "Reads a byte array with MessagePack format. Returns the read value." 24 | [bs] 25 | (when bs 26 | (let [in (ByteArrayInputStream. bs) 27 | reader (transit/reader in TRANSIT-FORMAT)] 28 | (transit/read reader)))) 29 | 30 | (defn receive 31 | "Reads a DatagramPacket off the provided socket. Logs the output otherwise 32 | the exception. Decodes the message, associating the sender's address onto the 33 | message map, then returns this map." 34 | [socket] 35 | (let [^DatagramPacket packet (udp/receive socket udp/MAX-BUFFER-SIZE)] 36 | (try 37 | (let [addr (let [^InetSocketAddress sa (.getSocketAddress packet)] 38 | (str (.getHostName sa) ":" (.getPort sa))) 39 | msg (-> (decode (.getData packet)) (assoc :addr addr))] 40 | (log/info "Received message ->" msg) 41 | msg) 42 | (catch Exception e 43 | (log/error e "Could not receive message"))))) 44 | 45 | (defn send 46 | "Given a socket, address, and a message, sends the message. The socket 47 | should be a DatagramSocket, e.g. as returned by udp/socket. The address 48 | should be a string in the format host:port. Finally the message should be a 49 | map, e.g. as returned by message/new. Logs the sent message, otherwise the 50 | exception." 51 | [socket address msg] 52 | (try 53 | (let [[host port] (util/parse-addr address) 54 | bs (encode msg)] 55 | (udp/send socket host port bs) 56 | (log/info "Sent message ->" msg "to" address)) 57 | (catch Exception e 58 | (log/error e "Could not send message")))) 59 | 60 | (defn new 61 | "Returns a new message map, given a key, value, and TTL." 62 | [k v ttl] 63 | {:k k :v v :ttl ttl}) 64 | -------------------------------------------------------------------------------- /src/quanta/node.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.node 2 | "Node logic." 3 | (:require [clojure.tools.logging :as log] 4 | [clojure.string :as string] 5 | [quanta.database :as database] 6 | [quanta.handler :as handler] 7 | [quanta.http :as http] 8 | [quanta.message :as message] 9 | [quanta.udp :as udp] 10 | [quanta.util :as util]) 11 | (:import [quanta.database LevelDBStore] 12 | [java.net DatagramSocket] 13 | [org.eclipse.jetty.server Server])) 14 | 15 | ;; 16 | ;; Node lifecycle. 17 | ;; 18 | 19 | (defn new 20 | "Creates a new node map with a main loop function. This may be passed to the 21 | start function to setup necessary runtime processes and execute the main 22 | loop. Likewise it may also be passed to the stop function to reverse this." 23 | [node-addr http-addr peers] 24 | {:node-addr node-addr 25 | :http-addr http-addr 26 | :peers (database/memory-store (atom peers)) 27 | :main #(future 28 | (try (while true 29 | (->> (message/receive (:socket %)) 30 | (handler/update-peer-list! %) 31 | (handler/handle-message %) 32 | (handler/send-responses %))) 33 | (catch Exception e 34 | (log/error e))))}) 35 | 36 | (defn start 37 | "Given a node map, starts all the processes needed and adds them to the map 38 | if the node has not already been started. Otherwise returns the provided map 39 | unaltered. Calling this function is idempotent." 40 | [{:keys [http-addr main node-addr running] :as node}] 41 | (if running 42 | node 43 | (let [[host port] (util/parse-addr node-addr) 44 | socket (udp/socket host port) 45 | node (assoc node 46 | :socket socket 47 | :store (database/leveldb-store 48 | (format ".quanta-primary-%s-%d" host port) 49 | (format ".quanta-trigram-%s-%d" host port)))] 50 | 51 | ;; Bootstrap peer list by requesting a random peer's peer list. Note that 52 | ;; this triggers additional traffic since each received message will in 53 | ;; turn generate heartbeat messages to other random peers. 54 | (when-let [peer (handler/rand-peer node)] 55 | (log/info "Bootstrapping peerlist") 56 | (message/send socket peer {:k "n:%" :v {} :ttl 1})) 57 | 58 | (-> node 59 | (assoc :running (main node)) 60 | (assoc :server (apply (partial http/new node) 61 | (util/parse-addr http-addr))))))) 62 | 63 | (defn stop 64 | "Given a node map, stops all the active proccesses and removes them from the 65 | map if the node has been started. Otherwise returns the provided map 66 | unaltered. Calling this function is idempotent." 67 | [{:keys [running 68 | ^Server server 69 | ^DatagramSocket socket 70 | ^LevelDBStore store] :as node}] 71 | (if running 72 | (do (future-cancel running) 73 | (.stop server) 74 | (.close store) 75 | (.close socket) 76 | (dissoc node :running :server :socket :store)) 77 | node)) 78 | 79 | (def ^{:doc "Stops then starts a given node map."} 80 | restart 81 | (comp start stop)) 82 | -------------------------------------------------------------------------------- /src/quanta/udp.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.udp 2 | "Basic User Datagram Protocol utilities." 3 | (:refer-clojure :exclude [send]) 4 | (:import [java.net DatagramPacket DatagramSocket InetAddress])) 5 | 6 | (def ^{:const true} MAX-BUFFER-SIZE 1500) 7 | 8 | (defn socket 9 | "Returns a DatagramSocket either unbound or otherwise bound to the given 10 | host and port." 11 | ([] 12 | (DatagramSocket.)) 13 | ([host port] 14 | (let [address (InetAddress/getByName host)] 15 | (DatagramSocket. port address)))) 16 | 17 | (defn- string->inet-address ^InetAddress 18 | [host] 19 | (InetAddress/getByName host)) 20 | 21 | (defn send 22 | "Sends a byte array via the provided socket to the given host and port." 23 | [^DatagramSocket s host ^Integer port ^bytes bs] 24 | (let [buffer-size (count bs) 25 | address (string->inet-address host) 26 | packet (DatagramPacket. bs buffer-size address port)] 27 | (.send s packet))) 28 | 29 | (defn receive 30 | "Returns a DatagramPacket via the provided socket. When larger than 31 | buffer-size, the data will be truncated." 32 | [^DatagramSocket s buffer-size] 33 | (let [buffer (byte-array buffer-size) 34 | packet (DatagramPacket. buffer buffer-size)] 35 | (.receive s packet) 36 | packet)) 37 | 38 | (defn packet->string 39 | "Returns the String coercion of the given DatagramPacket's getData value." 40 | [^DatagramPacket packet] 41 | (String. (.getData packet))) 42 | 43 | (comment 44 | (with-open [s (socket "localhost" 3000)] 45 | (let [r (future (packet->string (receive s 1024)))] 46 | (send s "localhost" 3000 (.getBytes "Hello, UDP world!")) 47 | @r))) 48 | -------------------------------------------------------------------------------- /src/quanta/util.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.util 2 | "Utility functions." 3 | (:require [clojure.string :as string])) 4 | 5 | (defn parse-int [^String s] (Integer. s)) 6 | 7 | (defn parse-addr 8 | [addr] 9 | (let [[host port] (string/split addr #":")] 10 | (list host (parse-int port)))) 11 | -------------------------------------------------------------------------------- /test/quanta/test_database.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.test_database 2 | (:require [clj-leveldb :as level] 3 | [clojure.set :refer [union]] 4 | [clojure.test :refer [deftest is use-fixtures]] 5 | [quanta.database :as database])) 6 | 7 | (def ^{:private true} db (atom nil)) 8 | 9 | (defn db-fixture 10 | [test-fn] 11 | (let [paths [".test-primary.db" ".test-trigram.db"]] 12 | ;; Setup the test db instance. 13 | (with-open [store (apply database/leveldb-store paths)] 14 | (swap! db (constantly store)) 15 | (test-fn)) 16 | 17 | ;; Destroy the test db instance. 18 | (doseq [path paths] 19 | (level/destroy-db path)))) 20 | 21 | (use-fixtures :each db-fixture) 22 | 23 | (deftest test-n-grams 24 | (is (= (database/n-grams 1 "foobar") 25 | '((\f) (\o) (\o) (\b) (\a) (\r)))) 26 | (is (= (database/n-grams 3 "foobar") 27 | '((\f \o \o) (\o \o \b) (\o \b \a) (\b \a \r))))) 28 | 29 | (deftest test-trigram-keys 30 | (is (= (database/trigram-keys "foobar") '("foo" "oob" "oba" "bar")))) 31 | 32 | (deftest test-k->re-s 33 | ;; Note that we apply str to the regex pattern in order to check equality. 34 | ;; This is because Java's regex pattern objects do not implement equals. 35 | ;; 36 | ;; See: http://dev.clojure.org/jira/browse/CLJ-1182 37 | (is (= (update-in (database/k->re-s "foo.") [0] str) 38 | [(str #"foo[.]") "foo."])) 39 | (is (= (update-in (database/k->re-s "foo%") [0] str) 40 | [(str #"foo.*") "foo"])) 41 | (is (= (update-in (database/k->re-s "foo*") [0] str) 42 | [(str #"foo.*") "foo"]))) 43 | 44 | (deftest test-match 45 | (database/put! @db "fooqux" "a") 46 | (database/put! @db "foobar" "b") 47 | (database/put! @db "foobaz" "c") 48 | (database/put! @db "nada" "d") 49 | (is (= (database/match @db "missing%") ())) 50 | (is (= (database/match @db "foob%") '("foobar" "foobaz"))) 51 | (is (= (database/match @db "fo%") '("foobar" "fooqux" "foobaz"))) 52 | (is (= (database/match @db "nada%") '("nada")))) 53 | 54 | (deftest test-put-with-merge! 55 | (database/put! @db "foo" #{1 2 3}) 56 | (database/put-with-merge! @db "foo" #{1 5 3} union) 57 | (= (database/get @db "foo") #{1 2 3 5})) 58 | 59 | (deftest test-max-vector 60 | (database/put! @db "vector" {0 1 13 42}) 61 | (is (= (database/max-vector @db "missing" {}) nil)) 62 | (is (= (database/max-vector @db "vector" {}) {0 1 13 42})) 63 | (is (= (database/max-vector @db "vector" {0 0 13 42}) {0 1})) 64 | (is (= (database/max-vector @db "vector" {0 1 13 42}) {}))) 65 | 66 | (deftest test-update-vector! 67 | ;; Ensure inserting a fresh vector populates the db. 68 | (is (= (database/update-vector! @db "vector" {0 1 13 42}) {0 1 13 42})) 69 | (is (= (database/get @db "vector") {0 1 13 42})) 70 | 71 | ;; Ensure only updates are returned. 72 | (is (database/update-vector! @db "vector" {0 0 13 42}) {0 1}) 73 | (is (= (database/get @db "vector") {0 1 13 42})) 74 | 75 | ;; Ensure partial updates work correctly. 76 | (is (= (database/update-vector! @db "vector" {0 2}) {0 2})) 77 | (is (= (database/get @db "vector") {0 2 13 42})) 78 | 79 | ;; Ensure an empty vector does not change stored vector. 80 | (is (= (database/update-vector! @db "vector" {}) {})) 81 | (is (= (database/get @db "vector") {0 2 13 42})) 82 | 83 | ;; Ensure adding new indices updates the store vector. 84 | (is (= (database/update-vector! @db "vector" {3 7}) {3 7})) 85 | (is (= (database/get @db "vector") {0 2 3 7 13 42}))) 86 | -------------------------------------------------------------------------------- /test/quanta/test_handler.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.test_handler 2 | (:require [clojure.test :refer [deftest is]] 3 | [quanta.handler :as handler])) 4 | 5 | (deftest test-addr->peer-key 6 | (is (= (handler/addr->peer-key "localhost:3000") 7 | "n:localhost:3000"))) 8 | 9 | (deftest test-peer-key->addr 10 | (is (= (handler/peer-key->addr "n:localhost:3000") 11 | "localhost:3000"))) 12 | 13 | (deftest test-rand-peer 14 | (let [node {:node-addr "localhost:3000" 15 | :peers (atom {"n:localhost:3000" {} 16 | "n:localhost:3001" {} 17 | "n:localhost:3002" {}})}] 18 | 19 | ;; Ensure the node's address is excluded. 20 | (is (every? nil? (repeatedly 100 #(some #{(:node-addr node)} 21 | (handler/rand-peer node))))))) 22 | 23 | (deftest test-heartbeat? 24 | (is (true? (handler/heartbeat? "n:foo"))) 25 | (is (false? (handler/heartbeat? "foo")))) 26 | -------------------------------------------------------------------------------- /test/quanta/test_message.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.test_message 2 | (:require [clojure.test :refer [deftest is]] 3 | [quanta.message :as message] 4 | [quanta.udp :as udp] 5 | [quanta.util :as util])) 6 | 7 | (deftest test-encoding-decoding 8 | (is (instance? (Class/forName "[B") (message/encode ""))) 9 | (is (= (-> {:foo :bar} message/encode message/decode) {:foo :bar}))) 10 | 11 | (deftest test-received-sent 12 | ;; For now use a real socket, but this should probably be stubbed out in the 13 | ;; future. 14 | (let [addr "localhost:6060" 15 | socket (apply udp/socket (util/parse-addr addr)) 16 | msg (future (message/receive socket)) 17 | expect (message/new "foo" "bar" 0)] 18 | ;; 1. Send a message to the socket. 19 | (message/send socket addr expect) 20 | 21 | ;; 2. Ensure the sent message is received by the future. 22 | (is (= @msg (assoc expect :addr addr))))) 23 | -------------------------------------------------------------------------------- /test/quanta/test_node.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.test_node 2 | (:require [clojure.test :refer [deftest is]] 3 | [quanta.message :as message] 4 | [quanta.node :as node])) 5 | 6 | (deftest test-node 7 | (let [peers-a {"n:localhost:4344" {0 0}} 8 | peers-b {"n:localhost:4343" {0 0}} 9 | a (node/new "localhost:4343" "localhost:4353" peers-a) 10 | b (node/new "localhost:4344" "localhost:4354" peers-b)] 11 | 12 | (is (= (:node-addr a) "localhost:4343")) 13 | (is (= (:node-addr b) "localhost:4344")) 14 | 15 | ;; Start the test nodes. 16 | (doseq [n [a b]] 17 | (node/start n)) 18 | 19 | (is (some @(:peers a) (keys peers-a))) 20 | (is (some @(:peers b) (keys peers-b))) 21 | 22 | ;; Stop test nodes. 23 | (doseq [n [a b]] 24 | (node/stop n)))) 25 | -------------------------------------------------------------------------------- /test/quanta/test_util.clj: -------------------------------------------------------------------------------- 1 | (ns quanta.test_util 2 | (:require [clojure.test :refer [deftest is]] 3 | [quanta.util :as util])) 4 | 5 | (deftest test-parse-int 6 | (is (= (util/parse-int "42") 42))) 7 | 8 | (deftest test-parse-addr 9 | (is (= (util/parse-addr "localhost:3333") 10 | ["localhost" 3333]))) 11 | --------------------------------------------------------------------------------