├── doc └── intro.md ├── .gitignore ├── src ├── test │ └── clojure │ │ └── clara │ │ ├── sample_ruleset.clj │ │ └── test_clara_storm.clj └── main │ ├── java │ └── clara │ │ └── storm │ │ ├── QueryClient.java │ │ └── RuleBolts.java │ └── clojure │ └── clara │ └── rules │ ├── storm_java.clj │ └── storm.clj ├── project.clj └── README.md /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to clara-storm 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | *.*~ -------------------------------------------------------------------------------- /src/test/clojure/clara/sample_ruleset.clj: -------------------------------------------------------------------------------- 1 | (ns clara.sample-ruleset 2 | (:use clara.rules 3 | clara.rules.testfacts) 4 | (:refer-clojure :exclude [==]) 5 | (import [clara.rules.testfacts Temperature WindSpeed Cold ColdAndWindy LousyWeather])) 6 | 7 | ;;; These rules are used for unit testing loading from a namespace. 8 | (defquery freezing-locations 9 | [] 10 | (Temperature (< temperature 32) (== ?loc location))) 11 | 12 | (defrule is-cold-and-windy 13 | (Temperature (< temperature 32) (== ?t temperature) (== ?loc location)) 14 | (WindSpeed (> windspeed 30) (== ?w windspeed) (== ?loc location)) 15 | => 16 | (insert! (->ColdAndWindy ?t ?w))) 17 | 18 | (defquery find-cold-and-windy 19 | [] 20 | (?fact <- ColdAndWindy)) 21 | 22 | (defrule is-lousy 23 | (ColdAndWindy (= temperature 15)) 24 | => 25 | (insert! (->LousyWeather))) 26 | 27 | (defquery find-lousy-weather 28 | [] 29 | (?fact <- LousyWeather)) 30 | -------------------------------------------------------------------------------- /src/main/java/clara/storm/QueryClient.java: -------------------------------------------------------------------------------- 1 | package clara.storm; 2 | 3 | import clara.rules.QueryResult; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Client for querying a Clara session running on a distributed cluster. 9 | */ 10 | public interface QueryClient { 11 | 12 | /** 13 | * Runs the query by the given name against the working memory and returns the matching 14 | * results. Query names are structured as "namespace/name" 15 | * 16 | * @param queryName the name of the query to perform, formatted as "namespace/name". 17 | * @param arguments query arguments 18 | * @return a list of query results 19 | */ 20 | public Iterable query(String queryName, Map arguments); 21 | 22 | /** 23 | * Runs the query by the given name against the working memory and returns the matching 24 | * results. Query names are structured as "namespace/name" 25 | * 26 | * @param queryName the name of the query to perform, formatted as "namespace/name". 27 | * @return a list of query results 28 | */ 29 | public Iterable query(String queryName); 30 | } 31 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.toomuchcode/clara-storm "0.1.0-SNAPSHOT" 2 | :description "Clara Rules -- Storm Support" 3 | :url "http://rbrush.github.io/clara-storm/" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.5.1"] 7 | [org.toomuchcode/clara-rules "0.3.0-SNAPSHOT"] 8 | [storm "0.8.2"]] 9 | :plugins [[codox "0.6.4"] 10 | [lein-javadoc "0.1.1"]] 11 | :javadoc-opts {:package-names ["clara.storm"]} 12 | :source-paths ["src/main/clojure"] 13 | :test-paths ["src/test/clojure"] 14 | :java-source-paths ["src/main/java"] 15 | :scm {:name "git" 16 | :url "https://github.com/rbrush/clara-storm.git"} 17 | :pom-addition [:developers [:developer {:id "rbrush"} 18 | [:name "Ryan Brush"] 19 | [:url "http://www.toomuchcode.org"]]] 20 | :repositories [["snapshots" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}]] 21 | :deploy-repositories [["snapshots" {:url "https://oss.sonatype.org/content/repositories/snapshots/" 22 | :creds :gpg}]]) 23 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/storm_java.clj: -------------------------------------------------------------------------------- 1 | (ns clara.rules.storm-java 2 | "Support for the Java-based API. Users should not use this namespace directly." 3 | (:require [clara.rules.storm :as storm] 4 | [clara.rules.engine :as eng]) 5 | (:import [clara.storm QueryClient] 6 | [clara.rules QueryResult])) 7 | 8 | 9 | (deftype JavaQueryResult [result] 10 | QueryResult 11 | (getResult [_ fieldName] 12 | (get result (keyword fieldName))) 13 | Object 14 | (toString [_] 15 | (.toString result))) 16 | 17 | (defn- run-query [drpc stream-name rulebase name args] 18 | 19 | (let [results 20 | (storm/query-storm 21 | drpc 22 | stream-name 23 | rulebase 24 | (or (deref (resolve (symbol name))) 25 | (throw (IllegalArgumentException. 26 | (str "Unable to resolve symbol to query: " name)))) 27 | args)] 28 | (map #(JavaQueryResult. %) results))) 29 | 30 | (deftype RPCQueryClient [drpc stream-name rulebase] 31 | QueryClient 32 | (query [this name args] 33 | (run-query drpc stream-name rulebase name args)) 34 | 35 | (query [this name] 36 | (run-query drpc stream-name rulebase name {}))) 37 | 38 | (defn attach-topology 39 | "Attach to the storm topology, and return a QueryClient instance to run queries against it." 40 | [builder drpc fact-source-ids query-source-id rulesets] 41 | 42 | (storm/attach-topology 43 | builder 44 | {:fact-source-ids fact-source-ids 45 | :query-source-id query-source-id 46 | :rulesets (map symbol rulesets)}) 47 | 48 | (RPCQueryClient. drpc "test" (apply eng/load-rules (map symbol rulesets)))) ; FIXME: use correct stream name. 49 | 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | clara-storm was an experiment in distributing production rule working memory in storm, but has not been maintained and may not work against newer versions of either Clara or Storm. 3 | 4 | Of course, users may use Clara as a library with Storm -- or any other JVM-based system -- but this project has been tabled due to lack of time. I can imagine it be resurrected in the future in some updated form but there are no immediate plans to do so. 5 | 6 | # clara-storm 7 | 8 | A proof-of-concept [Clara Rules](https://github.com/rbrush/clara-rules) host that allows running of rules over a the [Storm processing system](http://storm-project.net). 9 | 10 | ## How it works 11 | See the [Clara Rules](https://github.com/rbrush/clara-rules) documentation for an understanding of the rules engine itself. This project distributes the engine's working memory across a Storm topology, making it possible to scale and process very large data streams with simple, declarative rules. 12 | 13 | Clara makes distributed rules possible with some simple constraints in the engine itself. First, all facts used by Clara are immutable, which greatly simplifies sharing in a distributed environment 14 | 15 | Second, Clara requires that all joins between facts are hash-based. For instance, if I want to join a TemperatureReading fact and a Location fact, they must share a field that can be joined. The clara-storm host will route all join operations with matching fact bindings to the same bolt, so the join can occur there. The [sensors example](https://github.com/rbrush/clara-examples/blob/master/src/clara/examples/sensors.clj) shows such joins in action. 16 | 17 | This project models a distributed-memory rules engine in Storm by simply splitting it across an arbitrarily large number of _clara-bolts_. These bolts each hold a subset of the working memory, split up by hashing the fields that are used in rule joins. If a rule or constraint fires that creates new join values, they are sent to the appropriate _clara-bolt_ instance that contains that subset of the working memory. 18 | 19 | The result is a processing topology that is somewhat atypical for Storm: rather than a deep graph of processing steps, we have a large number of peer clara-bolts responsible for a subset of data. These peers then share the output of their processing with eachother based on the hash-based joins. 20 | 21 | ## Usage 22 | 23 | This is a proof-of-concept to exercise Clara over distributed systems. Usage information will come as this project progresses. 24 | 25 | ## License 26 | 27 | Copyright © 2013 Ryan Brush 28 | 29 | Distributed under the Eclipse Public License, the same as Clojure. 30 | -------------------------------------------------------------------------------- /src/main/java/clara/storm/RuleBolts.java: -------------------------------------------------------------------------------- 1 | package clara.storm; 2 | 3 | import backtype.storm.LocalDRPC; 4 | import backtype.storm.topology.TopologyBuilder; 5 | import backtype.storm.utils.DRPCClient; 6 | import clojure.lang.IFn; 7 | import clojure.lang.RT; 8 | import clojure.lang.Symbol; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | /** 14 | * Support for running Clara rules in Storm. 15 | */ 16 | public class RuleBolts { 17 | 18 | /** 19 | * Function to make a new Clara session. 20 | */ 21 | private static final IFn attachTopology; 22 | 23 | static { 24 | 25 | IFn require = RT.var("clojure.core", "require"); 26 | 27 | require.invoke(Symbol.intern("clara.rules.storm-java")); 28 | 29 | attachTopology = RT.var("clara.rules.storm-java", "attach-topology"); 30 | } 31 | 32 | /** 33 | * Attach a set of rules to a topology. This is primary for local testing with the given RPC client. 34 | * 35 | * @param builder the topology builder to attach the rules to the rules session. 36 | * @param drpc A distributed RPC client used to query 37 | * @param factSourceIds the identifiers of spouts or bolts that produce items on the "facts" stream used by the rule engine. 38 | * @param querySourceId the identifier of the DRPC used for issuing queries to the topology 39 | * @param rulesets One or more Clojure namespaces containing Clara rules. The namespaces must be visible in the caller's classloader 40 | * @return a client for issuing queries to the rule engine. 41 | */ 42 | public static QueryClient attach(TopologyBuilder builder, 43 | LocalDRPC drpc, 44 | List factSourceIds, 45 | String querySourceId, 46 | String... rulesets) { 47 | 48 | return (QueryClient) attachTopology.invoke(builder, drpc, factSourceIds, querySourceId, Arrays.asList(rulesets)); 49 | } 50 | 51 | /** 52 | * Attach a set of rules to a topology, without using a query client. 53 | * 54 | * @param builder the topology builder to attach the rules to 55 | * @param factSourceIds the identifiers of spouts or bolts that produce items on the "facts" stream used by the rule engine. 56 | * @param querySourceId the identifier of the DRPC used for issuing queries to the topology 57 | * @param rulesets One or more Clojure namespaces containing Clara rules. The namespaces must be visible in the caller's classloader 58 | */ 59 | public static void attach(TopologyBuilder builder, 60 | List factSourceIds, 61 | String... rulesets) { 62 | 63 | attachTopology.invoke(builder, null, factSourceIds, null, Arrays.asList(rulesets)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/clojure/clara/test_clara_storm.clj: -------------------------------------------------------------------------------- 1 | (ns clara.test-clara-storm 2 | (:use clojure.test clara.rules.storm) 3 | (:import [backtype.storm.drpc ReturnResults DRPCSpout 4 | LinearDRPCTopologyBuilder]) 5 | (:import [backtype.storm LocalDRPC LocalCluster StormSubmitter] 6 | [backtype.storm.topology TopologyBuilder]) 7 | (:import [clara.rules.testfacts Temperature WindSpeed Cold ColdAndWindy LousyWeather]) 8 | (:use [backtype.storm bootstrap testing]) 9 | (:use [backtype.storm.daemon common]) 10 | (:use [backtype.storm clojure config]) 11 | (:require [clara.rules.engine :as eng] 12 | [clara.sample-ruleset] 13 | [clara.rules.testfacts :as facts])) 14 | 15 | (defspout fact-spout {FACT-STREAM ["fact"]} 16 | [conf context collector] 17 | (let [] 18 | (spout 19 | (nextTuple [] 20 | (Thread/sleep 100) 21 | (emit-spout! collector [[(facts/->Temperature 20 "MCI")]] :stream FACT-STREAM) 22 | (emit-spout! collector [[(facts/->WindSpeed 40 "MCI")]] :stream FACT-STREAM)) 23 | 24 | (ack [id])))) 25 | 26 | (defn mk-test-topology 27 | [drpc] 28 | 29 | (let [builder (TopologyBuilder.)] 30 | (.setSpout builder "drpc" drpc nil) 31 | (.setSpout builder "facts" fact-spout nil) 32 | (attach-topology builder 33 | {:fact-source-ids ["facts"] 34 | :query-source-id (if drpc "drpc" nil) 35 | :rulesets ['clara.sample-ruleset]}) 36 | (.createTopology builder))) 37 | 38 | (deftest test-simple-query [] 39 | (let [drpc (LocalDRPC.) 40 | spout (DRPCSpout. "test" drpc) 41 | cluster (LocalCluster.) 42 | 43 | test-topology (mk-test-topology spout) 44 | 45 | rulebase (eng/load-rules 'clara.sample-ruleset)] 46 | 47 | (.submitTopology cluster "test" {} test-topology) 48 | 49 | ;; Let some events process. 50 | (Thread/sleep 2000) 51 | 52 | ;; Ensure the query matches as expected. 53 | (is (= {:?loc "MCI"} 54 | (first (query-storm drpc "test" rulebase clara.sample-ruleset/freezing-locations {})))) 55 | 56 | (.shutdown cluster) 57 | (.shutdown drpc))) 58 | 59 | (deftest test-join-query [] 60 | (let [drpc (LocalDRPC.) 61 | spout (DRPCSpout. "test" drpc) 62 | cluster (LocalCluster.) 63 | 64 | test-topology (mk-test-topology spout) 65 | 66 | rulebase (eng/load-rules 'clara.sample-ruleset)] 67 | 68 | (.submitTopology cluster "test" {} test-topology) 69 | 70 | ;; Let some events process. 71 | (Thread/sleep 4000) 72 | 73 | ;; Ensure the query matches as expected. 74 | (is (= {:?fact #clara.rules.testfacts.ColdAndWindy{:temperature 20, :windspeed 40}} 75 | (first (query-storm drpc "test" rulebase clara.sample-ruleset/find-cold-and-windy {})))) 76 | 77 | (.shutdown cluster) 78 | (.shutdown drpc))) 79 | 80 | (deftest test-java-client [] 81 | (let [drpc (LocalDRPC.) 82 | spout (DRPCSpout. "test" drpc) 83 | cluster (LocalCluster.) 84 | 85 | ; Wire up topology with our drpc and fact spout. 86 | builder (doto (TopologyBuilder.) 87 | (.setSpout "drpc" spout nil) 88 | (.setSpout "facts" fact-spout nil)) 89 | 90 | ;; Attach our Clara rules to the facts and drpc and 91 | ;; get our query client. 92 | client (clara.storm.RuleBolts/attach builder 93 | drpc 94 | ["facts"] 95 | "drpc" 96 | (doto (make-array String 1) 97 | (aset 0 "clara.sample-ruleset"))) 98 | test-topology (.createTopology builder)] 99 | 100 | ;; Run the topology. 101 | (.submitTopology cluster "test" {} test-topology) 102 | 103 | ;; Let some events process. 104 | (Thread/sleep 2000) 105 | 106 | ;; Ensure the query matches as expected. 107 | (is (= "MCI" 108 | (.getResult 109 | (first (.query client "clara.sample-ruleset/freezing-locations")) 110 | "?loc"))) 111 | 112 | (.shutdown cluster) 113 | (.shutdown drpc))) 114 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/storm.clj: -------------------------------------------------------------------------------- 1 | (ns clara.rules.storm 2 | (:require [clara.rules.engine :as eng] 3 | [clara.rules.memory :as mem] 4 | [clojure.set :as set] 5 | [clojure.edn :as edn] 6 | [backtype.storm.clojure :refer [emit-bolt! ack! defbolt bolt bolt-spec]]) 7 | (:import [clara.rules.engine ITransport LocalTransport ProductionNode] 8 | [backtype.storm.drpc ReturnResults DRPCSpout LinearDRPCTopologyBuilder] 9 | [backtype.storm.generated GlobalStreamId Grouping NullStruct] 10 | [backtype.storm.utils Utils])) 11 | 12 | (def FACT-STREAM "fact") 13 | (def TOKEN-STREAM "token") 14 | (def WME-STREAM "wme") 15 | (def QUERY-STREAM "query") 16 | 17 | ;; TODO: transport should determine if item can be processed locally, 18 | ;; e.g. any node that doesn't perform a join operation. 19 | 20 | (deftype StormTransport [collector anchor] 21 | ITransport 22 | (send-elements [transport memory nodes elements] 23 | (doseq [[bindings element-group] (group-by :bindings elements) 24 | node nodes 25 | :let [join-bindings (select-keys bindings (eng/get-join-keys node))]] 26 | (emit-bolt! collector [(:id node) join-bindings element-group true] :anchor anchor :stream WME-STREAM))) 27 | 28 | (send-tokens [transport memory nodes tokens] 29 | (doseq [[bindings token-group] (group-by :bindings tokens) 30 | node nodes 31 | :let [join-bindings (select-keys bindings (eng/get-join-keys node))]] 32 | (emit-bolt! collector [(:id node) join-bindings token-group true] :anchor anchor :stream TOKEN-STREAM))) 33 | 34 | (retract-elements [transport memory nodes elements] 35 | (doseq [[bindings element-group] (group-by :bindings elements) 36 | node nodes 37 | :let [join-bindings (select-keys bindings (eng/get-join-keys node))]] 38 | (emit-bolt! collector [(:id node) join-bindings element-group false] :anchor anchor :stream WME-STREAM))) 39 | 40 | (retract-tokens [transport memory nodes tokens] 41 | (doseq [[bindings token-group] (group-by :bindings tokens) 42 | node nodes 43 | :let [join-bindings (select-keys bindings (eng/get-join-keys node))]] 44 | (emit-bolt! collector [(:id node) join-bindings token-group false] :anchor anchor :stream TOKEN-STREAM)))) 45 | 46 | 47 | (defbolt clara-bolt {WME-STREAM ["node-id" "bindings" "elements" "activation"] 48 | TOKEN-STREAM ["node-id" "bindings" "tokens" "activation"] 49 | QUERY-STREAM ["result" "return-info"]} 50 | {:prepare :true :params [rules]} 51 | [conf context collector] 52 | (let [session (apply eng/load-rules rules) 53 | bolt-memory (atom (eng/local-memory session (LocalTransport.))) 54 | get-alphas-fn (eng/create-get-alphas-fn type session) ;; TODO: use a different type function. 55 | hash-to-node (:id-to-node session)] 56 | (bolt 57 | (execute [tuple] 58 | (let [memory (mem/to-transient @bolt-memory) 59 | transport (StormTransport. collector tuple)] 60 | (condp = (.getSourceStreamId tuple) 61 | 62 | FACT-STREAM 63 | (doseq [[cls fact-group] (group-by class (.getValue tuple 0)) 64 | root (get-in session [:alpha-roots cls])] 65 | (eng/alpha-activate root fact-group memory transport)) 66 | 67 | TOKEN-STREAM 68 | (let [node (hash-to-node (.getValue tuple 0)) 69 | bindings (.getValue tuple 1) 70 | tokens (.getValue tuple 2) 71 | activation (.getValue tuple 3)] 72 | 73 | (if activation 74 | (eng/left-activate node bindings tokens memory transport) 75 | (eng/left-retract node bindings tokens memory transport)) 76 | 77 | ;; If the node is a production node, fire the rules. 78 | (when (isa? (class node) ProductionNode) 79 | (eng/fire-rules* session [node] memory transport get-alphas-fn))) 80 | 81 | WME-STREAM 82 | (let [node (hash-to-node (.getValue tuple 0)) 83 | join-bindings (.getValue tuple 1) 84 | elements (.getValue tuple 2) 85 | activation (.getValue tuple 3)] 86 | 87 | (if activation 88 | (eng/right-activate node join-bindings elements memory transport) 89 | (eng/right-retract node join-bindings elements memory transport))) 90 | 91 | QUERY-STREAM 92 | (let [node (hash-to-node (.getValue tuple 0)) 93 | params (.getValue tuple 1) 94 | return-info (.getValue tuple 2) 95 | result (map :bindings (mem/get-tokens memory node params))] 96 | 97 | (emit-bolt! collector 98 | [(pr-str result) return-info] 99 | :anchor tuple 100 | :stream QUERY-STREAM))) 101 | 102 | ;; Update the node memory to include the changes. 103 | (reset! bolt-memory (mem/to-persistent! memory)) 104 | (ack! collector tuple)))))) 105 | 106 | (defbolt query-bolt {QUERY-STREAM ["node-id" "bindings" "return-info"]} 107 | {:params [rules]} 108 | 109 | [tuple collector] 110 | (let [[node-id bindings] (read-string (.getValue tuple 0)) 111 | return-info (.getValue tuple 1)] 112 | (emit-bolt! collector 113 | [node-id bindings return-info] 114 | :anchor tuple 115 | :stream QUERY-STREAM) 116 | (ack! collector tuple))) 117 | 118 | (defn- mk-inputs [inputs] 119 | (into {} 120 | (for [[stream-id grouping-spec] inputs] 121 | [(if (sequential? stream-id) 122 | (GlobalStreamId. (first stream-id) (second stream-id)) 123 | (GlobalStreamId. stream-id Utils/DEFAULT_STREAM_ID)) 124 | (Grouping/shuffle (NullStruct.))]))) 125 | 126 | (defn- add-groupings [declarer inputs] 127 | (doseq [[id grouping] (mk-inputs inputs)] 128 | (.grouping declarer id grouping))) 129 | 130 | 131 | (defn attach-topology 132 | "Attach the pipeline to the topology, using logic drawn from Storm's mk-topology function" 133 | [builder {:keys [fact-source-ids query-source-id rulesets]}] 134 | 135 | ;; Create a bolt map that includes the query source and returner if specified. 136 | (let [bolt-map (if query-source-id 137 | {"query-bolt" (bolt-spec {query-source-id :shuffle} (query-bolt rulesets)) 138 | 139 | "clara-bolt" (bolt-spec 140 | (into 141 | {["query-bolt" QUERY-STREAM] ["node-id" "bindings"], 142 | ["clara-bolt" WME-STREAM] ["node-id" "bindings"], 143 | ["clara-bolt" TOKEN-STREAM] ["node-id" "bindings"]} 144 | 145 | ;; Add all fact sources to the bolt spec. 146 | (for [fact-source-id fact-source-ids] 147 | [[fact-source-id FACT-STREAM] :shuffle])) 148 | 149 | (clara-bolt rulesets)) 150 | "query-returner" (bolt-spec {["clara-bolt" QUERY-STREAM] :shuffle} (new ReturnResults))} 151 | 152 | {"clara-bolt" (bolt-spec 153 | (into 154 | {["clara-bolt" WME-STREAM] ["node-id" "bindings"], 155 | ["clara-bolt" TOKEN-STREAM] ["node-id" "bindings"]} 156 | 157 | ;; Add all fact sources to the bolt spec. 158 | (for [fact-source-id fact-source-ids] 159 | [[fact-source-id FACT-STREAM] :shuffle])) 160 | 161 | (clara-bolt rulesets))})] 162 | 163 | ;; Add the bolts to the builder. 164 | (doseq [[name {bolt :obj p :p conf :conf inputs :inputs}] bolt-map] 165 | (-> builder 166 | (.setBolt name bolt (if-not (nil? p) (int p) p)) 167 | (.addConfigurations conf) 168 | (add-groupings inputs)))) 169 | 170 | builder) 171 | 172 | (defn query-storm [drpc name session query params] 173 | (let [query-node (get-in session [:query-nodes query])] 174 | 175 | ;; TODO: We should probably use the EDN reader here, although this is 176 | ;; an internal call. Should find a way to register the expected query results with EDN. 177 | (read-string (.execute drpc name (pr-str [(:id query-node) params]))))) 178 | --------------------------------------------------------------------------------