├── .gitignore
├── .midje.clj
├── .travis.yml
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── coveralls.sh
├── project.clj
├── src
└── clj_http_hystrix
│ └── core.clj
└── test
└── clj_http_hystrix
├── core_test.clj
└── util.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 |
--------------------------------------------------------------------------------
/.midje.clj:
--------------------------------------------------------------------------------
1 | (change-defaults :print-level :print-facts)
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: lein
3 | script: lein test
4 | after_script:
5 | - bash -ex coveralls.sh
6 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | * [Joe Littlejohn](https://github.com/joelittlejohn)
2 | * [Mark Tinsley](https://github.com/mt3593)
3 | * [Matt Kipps](https://github.com/mtkp)
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4 |
5 | 1. DEFINITIONS
6 |
7 | "Contribution" means:
8 |
9 | a) in the case of the initial Contributor, the initial code and
10 | documentation distributed under this Agreement, and
11 |
12 | b) in the case of each subsequent Contributor:
13 |
14 | i) changes to the Program, and
15 |
16 | ii) additions to the Program;
17 |
18 | where such changes and/or additions to the Program originate from and are
19 | distributed by that particular Contributor. A Contribution 'originates' from
20 | a Contributor if it was added to the Program by such Contributor itself or
21 | anyone acting on such Contributor's behalf. Contributions do not include
22 | additions to the Program which: (i) are separate modules of software
23 | distributed in conjunction with the Program under their own license
24 | agreement, and (ii) are not derivative works of the Program.
25 |
26 | "Contributor" means any person or entity that distributes the Program.
27 |
28 | "Licensed Patents" mean patent claims licensable by a Contributor which are
29 | necessarily infringed by the use or sale of its Contribution alone or when
30 | combined with the Program.
31 |
32 | "Program" means the Contributions distributed in accordance with this
33 | Agreement.
34 |
35 | "Recipient" means anyone who receives the Program under this Agreement,
36 | including all Contributors.
37 |
38 | 2. GRANT OF RIGHTS
39 |
40 | a) Subject to the terms of this Agreement, each Contributor hereby grants
41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
42 | reproduce, prepare derivative works of, publicly display, publicly perform,
43 | distribute and sublicense the Contribution of such Contributor, if any, and
44 | such derivative works, in source code and object code form.
45 |
46 | b) Subject to the terms of this Agreement, each Contributor hereby grants
47 | Recipient a non-exclusive, worldwide, royalty-free patent license under
48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
49 | transfer the Contribution of such Contributor, if any, in source code and
50 | object code form. This patent license shall apply to the combination of the
51 | Contribution and the Program if, at the time the Contribution is added by the
52 | Contributor, such addition of the Contribution causes such combination to be
53 | covered by the Licensed Patents. The patent license shall not apply to any
54 | other combinations which include the Contribution. No hardware per se is
55 | licensed hereunder.
56 |
57 | c) Recipient understands that although each Contributor grants the licenses
58 | to its Contributions set forth herein, no assurances are provided by any
59 | Contributor that the Program does not infringe the patent or other
60 | intellectual property rights of any other entity. Each Contributor disclaims
61 | any liability to Recipient for claims brought by any other entity based on
62 | infringement of intellectual property rights or otherwise. As a condition to
63 | exercising the rights and licenses granted hereunder, each Recipient hereby
64 | assumes sole responsibility to secure any other intellectual property rights
65 | needed, if any. For example, if a third party patent license is required to
66 | allow Recipient to distribute the Program, it is Recipient's responsibility
67 | to acquire that license before distributing the Program.
68 |
69 | d) Each Contributor represents that to its knowledge it has sufficient
70 | copyright rights in its Contribution, if any, to grant the copyright license
71 | set forth in this Agreement.
72 |
73 | 3. REQUIREMENTS
74 |
75 | A Contributor may choose to distribute the Program in object code form under
76 | its own license agreement, provided that:
77 |
78 | a) it complies with the terms and conditions of this Agreement; and
79 |
80 | b) its license agreement:
81 |
82 | i) effectively disclaims on behalf of all Contributors all warranties and
83 | conditions, express and implied, including warranties or conditions of title
84 | and non-infringement, and implied warranties or conditions of merchantability
85 | and fitness for a particular purpose;
86 |
87 | ii) effectively excludes on behalf of all Contributors all liability for
88 | damages, including direct, indirect, special, incidental and consequential
89 | damages, such as lost profits;
90 |
91 | iii) states that any provisions which differ from this Agreement are offered
92 | by that Contributor alone and not by any other party; and
93 |
94 | iv) states that source code for the Program is available from such
95 | Contributor, and informs licensees how to obtain it in a reasonable manner on
96 | or through a medium customarily used for software exchange.
97 |
98 | When the Program is made available in source code form:
99 |
100 | a) it must be made available under this Agreement; and
101 |
102 | b) a copy of this Agreement must be included with each copy of the Program.
103 |
104 | Contributors may not remove or alter any copyright notices contained within
105 | the Program.
106 |
107 | Each Contributor must identify itself as the originator of its Contribution,
108 | if any, in a manner that reasonably allows subsequent Recipients to identify
109 | the originator of the Contribution.
110 |
111 | 4. COMMERCIAL DISTRIBUTION
112 |
113 | Commercial distributors of software may accept certain responsibilities with
114 | respect to end users, business partners and the like. While this license is
115 | intended to facilitate the commercial use of the Program, the Contributor who
116 | includes the Program in a commercial product offering should do so in a
117 | manner which does not create potential liability for other Contributors.
118 | Therefore, if a Contributor includes the Program in a commercial product
119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend
120 | and indemnify every other Contributor ("Indemnified Contributor") against any
121 | losses, damages and costs (collectively "Losses") arising from claims,
122 | lawsuits and other legal actions brought by a third party against the
123 | Indemnified Contributor to the extent caused by the acts or omissions of such
124 | Commercial Contributor in connection with its distribution of the Program in
125 | a commercial product offering. The obligations in this section do not apply
126 | to any claims or Losses relating to any actual or alleged intellectual
127 | property infringement. In order to qualify, an Indemnified Contributor must:
128 | a) promptly notify the Commercial Contributor in writing of such claim, and
129 | b) allow the Commercial Contributor tocontrol, and cooperate with the
130 | Commercial Contributor in, the defense and any related settlement
131 | negotiations. The Indemnified Contributor may participate in any such claim
132 | at its own expense.
133 |
134 | For example, a Contributor might include the Program in a commercial product
135 | offering, Product X. That Contributor is then a Commercial Contributor. If
136 | that Commercial Contributor then makes performance claims, or offers
137 | warranties related to Product X, those performance claims and warranties are
138 | such Commercial Contributor's responsibility alone. Under this section, the
139 | Commercial Contributor would have to defend claims against the other
140 | Contributors related to those performance claims and warranties, and if a
141 | court requires any other Contributor to pay any damages as a result, the
142 | Commercial Contributor must pay those damages.
143 |
144 | 5. NO WARRANTY
145 |
146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
151 | appropriateness of using and distributing the Program and assumes all risks
152 | associated with its exercise of rights under this Agreement , including but
153 | not limited to the risks and costs of program errors, compliance with
154 | applicable laws, damage to or loss of data, programs or equipment, and
155 | unavailability or interruption of operations.
156 |
157 | 6. DISCLAIMER OF LIABILITY
158 |
159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
166 | OF SUCH DAMAGES.
167 |
168 | 7. GENERAL
169 |
170 | If any provision of this Agreement is invalid or unenforceable under
171 | applicable law, it shall not affect the validity or enforceability of the
172 | remainder of the terms of this Agreement, and without further action by the
173 | parties hereto, such provision shall be reformed to the minimum extent
174 | necessary to make such provision valid and enforceable.
175 |
176 | If Recipient institutes patent litigation against any entity (including a
177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
178 | (excluding combinations of the Program with other software or hardware)
179 | infringes such Recipient's patent(s), then such Recipient's rights granted
180 | under Section 2(b) shall terminate as of the date such litigation is filed.
181 |
182 | All Recipient's rights under this Agreement shall terminate if it fails to
183 | comply with any of the material terms or conditions of this Agreement and
184 | does not cure such failure in a reasonable period of time after becoming
185 | aware of such noncompliance. If all Recipient's rights under this Agreement
186 | terminate, Recipient agrees to cease use and distribution of the Program as
187 | soon as reasonably practicable. However, Recipient's obligations under this
188 | Agreement and any licenses granted by Recipient relating to the Program shall
189 | continue and survive.
190 |
191 | Everyone is permitted to copy and distribute copies of this Agreement, but in
192 | order to avoid inconsistency the Agreement is copyrighted and may only be
193 | modified in the following manner. The Agreement Steward reserves the right to
194 | publish new versions (including revisions) of this Agreement from time to
195 | time. No one other than the Agreement Steward has the right to modify this
196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
197 | Eclipse Foundation may assign the responsibility to serve as the Agreement
198 | Steward to a suitable separate entity. Each new version of the Agreement will
199 | be given a distinguishing version number. The Program (including
200 | Contributions) may always be distributed subject to the version of the
201 | Agreement under which it was received. In addition, after a new version of
202 | the Agreement is published, Contributor may elect to distribute the Program
203 | (including its Contributions) under the new version. Except as expressly
204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
205 | licenses to the intellectual property of any Contributor under this
206 | Agreement, whether expressly, by implication, estoppel or otherwise. All
207 | rights in the Program not expressly granted under this Agreement are
208 | reserved.
209 |
210 | This Agreement is governed by the laws of the State of New York and the
211 | intellectual property laws of the United States of America. No party to this
212 | Agreement will bring a legal action under this Agreement more than one year
213 | after the cause of action arose. Each party waives its rights to a jury trial
214 | in any resulting litigation.
215 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # clj-http-hystrix [](https://travis-ci.org/joelittlejohn/clj-http-hystrix) [](https://coveralls.io/r/joelittlejohn/clj-http-hystrix?branch=master)
2 |
3 | 
4 |
5 | A Clojure library to wrap clj-http requests as [hystrix](https://github.com/Netflix/Hystrix) commands whenever a request options map includes `:hystrix/...` keys.
6 |
7 | ## Usage
8 |
9 | When you start your app, add:
10 |
11 | ```clj
12 | (clj-http-hystrix.core/add-hook)
13 | ```
14 |
15 | Whenever you make an http request, add **one or more** of the hystrix-clj options to your options map, e.g.:
16 |
17 | ```clj
18 | (http/get "http://www.google.com" {:hystrix/command-key :default
19 | :hystrix/fallback-fn default-fallback
20 | :hystrix/group-key :default
21 | :hystrix/threads 10
22 | :hystrix/queue-size 5
23 | :hystrix/timeout-ms 1000
24 | :hystrix/breaker-request-volume 20
25 | :hystrix/breaker-error-percent 50
26 | :hystrix/breaker-sleep-window-ms 5000
27 | :hystrix/bad-request-pred client-error?})
28 | ```
29 |
30 | Requests without any `:hystrix/...` keys won't use Hystrix. If you include **at least one** `:hystrix/...` key then any keys not specified will take the above (default) values.
31 |
32 | Custom default values can be specified when registering with `add-hook`. Any keys you supply will override the defaults shown above:
33 |
34 | ```clj
35 | (clj-http-hystrix.core/add-hook {:hystrix/timeout-ms 2500
36 | :hystrix/queue-size 12})
37 | ```
38 |
39 | ## Bad requests
40 |
41 | Hystrix allows some failures to be marked as bad requests, that is, requests that have failed because of a badly formed request rather than an error in the downstream service[1](https://github.com/Netflix/Hystrix/wiki/How-To-Use#error-propagation). clj-http-hystrix allows a predicate to be supplied under the `:hystrix/bad-request-pred` key, and if this predicate returns `true` for a given request & response, then the failure will be considered a 'bad request' (and not counted towards the failure metrics for a command).
42 |
43 | By default, all client errors (4xx family of response codes) are considered Hystrix bad requests and are not counted towards the failure metrics for a command. There are some useful predicates and predicate generators provided[2](https://github.com/joelittlejohn/clj-http-hystrix/blob/18a4f8f9636e531558a57557681c5d5861b27e42/src/clj_http_hystrix/core.clj#L67).
44 |
45 | ## Cached vs dynamic configuration
46 |
47 | Hystrix caches configuration for a command and hence there are limits to how this library can react to configuration options that vary dynamically. For a given command-key, the `:hystrix/timeout-ms` will be fixed on first use. This means it's a bad idea to reuse the `:hystrix/command-key` value in many parts of your app. When you want a new configuration, you should use a new `:hystrix/command-key` value.
48 |
49 | The same is true for thread pools - configuration is cached per `:hystrix/group-key`, so if you need to use a different value for `:hystrix/queue-size` or `:hystrix/threads` then you should use a new `:hystrix/group-key` value.
50 |
51 | ## License
52 |
53 | Copyright © 2014 Joe Littlejohn, Mark Tinsley
54 |
55 | Distributed under the Eclipse Public License either version 1.0 or (at
56 | your option) any later version.
57 |
--------------------------------------------------------------------------------
/coveralls.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | lein cloverage --coveralls
3 | curl -F 'json_file=@target/coverage/coveralls.json' 'https://coveralls.io/api/v1/jobs'
4 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject clj-http-hystrix "0.1.7-SNAPSHOT"
2 |
3 | :description "A Clojure library to wrap clj-http requests as hystrix commands"
4 | :url "https://github.com/joelittlejohn/clj-http-hystrix"
5 | :license {:name "Eclipse Public License"
6 | :url "http://www.eclipse.org/legal/epl-v10.html"}
7 |
8 | :dependencies [[clj-http "3.10.0"]
9 | [com.netflix.hystrix/hystrix-clj "1.5.18"]
10 | [org.clojure/tools.logging "0.5.0"]
11 | [robert/hooke "1.3.0"]
12 | [slingshot "0.12.2"]]
13 |
14 | :env {:restdriver-port "8081"}
15 |
16 | :repositories [["releases" {:url "https://clojars.org/repo"
17 | :creds :gpg}]]
18 |
19 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.10.1"]
20 | [midje "1.9.9"]
21 | [rest-cljer "0.2.2" :exclusions [clj-http]]]
22 | :plugins [[lein-cloverage "1.0.10"]
23 | [lein-environ "1.1.0"]
24 | [lein-midje "3.2.1"]]}})
25 |
--------------------------------------------------------------------------------
/src/clj_http_hystrix/core.clj:
--------------------------------------------------------------------------------
1 | (ns clj-http-hystrix.core
2 | (:require [clj-http.client :as http]
3 | [clojure.tools.logging :refer [warn error]]
4 | [robert.hooke :as hooke]
5 | [slingshot.slingshot :refer [get-thrown-object]]
6 | [slingshot.support :refer [wrap stack-trace]])
7 | (:import [com.netflix.hystrix HystrixCommand
8 | HystrixThreadPoolProperties
9 | HystrixCommandProperties
10 | HystrixCommand$Setter
11 | HystrixCommandGroupKey$Factory
12 | HystrixCommandKey$Factory]
13 | [com.netflix.hystrix.exception HystrixBadRequestException]
14 | [org.slf4j MDC]))
15 |
16 | (defn default-fallback [req resp]
17 | (if (:status resp)
18 | resp
19 | {:status 503}))
20 |
21 | (defn client-error?
22 | "Returns true when the response has one of the 4xx family of status
23 | codes"
24 | [req resp]
25 | (http/client-error? resp))
26 |
27 | (defn ^:private handle-exception
28 | [f req]
29 | (let [^Exception raw-response (try (f) (catch Exception e e))
30 | resp (if (instance? HystrixBadRequestException raw-response)
31 | (get-thrown-object (.getCause raw-response))
32 | raw-response)]
33 | (when (:status resp)
34 | ((http/wrap-exceptions (constantly resp)) req))
35 | resp))
36 |
37 | (def ^:private ^:const hystrix-base-configuration
38 | {:hystrix/command-key :default
39 | :hystrix/fallback-fn default-fallback
40 | :hystrix/group-key :default
41 | :hystrix/threads 10
42 | :hystrix/queue-size 5
43 | :hystrix/timeout-ms 1000
44 | :hystrix/breaker-request-volume 20
45 | :hystrix/breaker-error-percent 50
46 | :hystrix/breaker-sleep-window-ms 5000
47 | :hystrix/bad-request-pred client-error?})
48 |
49 | (def ^:private hystrix-keys
50 | (keys hystrix-base-configuration))
51 |
52 | (defn ^:private hystrix-defaults [defaults]
53 | (merge hystrix-base-configuration (select-keys defaults hystrix-keys)))
54 |
55 | (defn ^:private group-key [s]
56 | (HystrixCommandGroupKey$Factory/asKey (name s)))
57 |
58 | (defn ^:private command-key [s]
59 | (HystrixCommandKey$Factory/asKey (name s)))
60 |
61 | (defn ^:private configurator
62 | "Create a configurator that can configure the hystrix according to the
63 | declarative config (or some sensible defaults)"
64 | ^HystrixCommand$Setter [config]
65 | (let [{group :hystrix/group-key
66 | command :hystrix/command-key
67 | timeout :hystrix/timeout-ms
68 | threads :hystrix/threads
69 | queue-size :hystrix/queue-size
70 | sleep-window :hystrix/breaker-sleep-window-ms
71 | error-percent :hystrix/breaker-error-percent
72 | request-volume :hystrix/breaker-request-volume} config
73 | command-configurator (doto (HystrixCommandProperties/Setter)
74 | (.withExecutionIsolationThreadTimeoutInMilliseconds timeout)
75 | (.withCircuitBreakerRequestVolumeThreshold request-volume)
76 | (.withCircuitBreakerErrorThresholdPercentage error-percent)
77 | (.withCircuitBreakerSleepWindowInMilliseconds sleep-window)
78 | (.withMetricsRollingPercentileEnabled false))
79 | thread-pool-configurator (doto (HystrixThreadPoolProperties/Setter)
80 | (.withCoreSize threads)
81 | (.withMaxQueueSize queue-size)
82 | (.withQueueSizeRejectionThreshold queue-size))]
83 | (doto (HystrixCommand$Setter/withGroupKey (group-key group))
84 | (.andCommandKey (command-key command))
85 | (.andCommandPropertiesDefaults command-configurator)
86 | (.andThreadPoolPropertiesDefaults thread-pool-configurator))))
87 |
88 | (defn ^:private log-error [command-name ^HystrixCommand context]
89 | (let [message (format "Failed to complete %s %s" command-name (.getExecutionEvents context))]
90 | (if-let [exception (.getFailedExecutionException context)]
91 | (warn exception message)
92 | (warn message))))
93 |
94 | (defn status-codes
95 | "Create a predicate that returns true whenever one of the given
96 | status codes is present"
97 | [& status-codes]
98 | (let [status-codes (set status-codes)]
99 | (fn [req resp]
100 | (contains? status-codes (:status resp)))))
101 |
102 | (defn- frame-setter
103 | "Returns a function that can be called to set the thread binding frame
104 | to the current thread binding frame."
105 | []
106 | (let [caller (Thread/currentThread)
107 | frame (clojure.lang.Var/cloneThreadBindingFrame)]
108 | (fn []
109 | (when-not (identical? caller (Thread/currentThread))
110 | (clojure.lang.Var/resetThreadBindingFrame frame)))))
111 |
112 | (defn- mdc-setter
113 | "Returns a function that can be called to set the MDC to the current MDC."
114 | []
115 | (let [mdc (MDC/getCopyOfContextMap)]
116 | (fn []
117 | (when mdc
118 | (MDC/setContextMap mdc)))))
119 |
120 | (defn hystrix-wrapper
121 | "Create a function that wraps clj-http client requests with hystrix features
122 | (but only if a hystrix key is present in the options map).
123 | Accepts a possibly empty map of default hystrix values that will be used as
124 | fallback configuration for each hystrix request."
125 | [custom-defaults]
126 | (let [defaults (hystrix-defaults custom-defaults)]
127 | (fn [f req]
128 | (if (not-empty (select-keys req hystrix-keys))
129 | (let [req (merge defaults req)
130 | bad-request-pred (:hystrix/bad-request-pred req)
131 | fallback (:hystrix/fallback-fn req)
132 | wrap-bad-request (fn [resp]
133 | (if (bad-request-pred req resp)
134 | (throw
135 | (HystrixBadRequestException.
136 | "Ignored bad request"
137 | (wrap {:object resp
138 | :message "Bad request pred"
139 | :stack-trace (stack-trace)})))
140 | resp))
141 | wrap-exception-response (fn [resp]
142 | ((http/wrap-exceptions (constantly resp))
143 | (assoc req :throw-exceptions true)))
144 | configurator (configurator req)
145 | set-frame (frame-setter)
146 | set-mdc (mdc-setter)
147 | command (proxy [HystrixCommand] [configurator]
148 | (getFallback []
149 | (set-frame)
150 | (set-mdc)
151 | (log-error (:hystrix/command-key req) this)
152 | (let [exception (.getFailedExecutionException ^HystrixCommand this)
153 | response (when exception (get-thrown-object exception))]
154 | (fallback req response)))
155 | (run []
156 | (set-frame)
157 | (set-mdc)
158 | (-> req
159 | (assoc :throw-exceptions false)
160 | f
161 | wrap-bad-request
162 | wrap-exception-response)))]
163 | (handle-exception #(.execute command) req))
164 | (f req)))))
165 |
166 | (defn add-hook
167 | "Activate clj-http-hystrix to wrap all clj-http client requests as
168 | hystrix commands.
169 | Provide custom hystrix defaults by providing an optional defaults map:
170 |
171 | ;; use clj-http-hystrix.core default configuration
172 | (add-hook)
173 |
174 | ;; 6 second timeout fallback, if not specified in request
175 | (add-hook {:hystrix/timeout-ms 6000})
176 | "
177 | ([] (add-hook {}))
178 | ([defaults]
179 | (hooke/add-hook #'http/request ::wrap-hystrix (hystrix-wrapper defaults))))
180 |
181 | (defn remove-hook
182 | "Deactivate clj-http-hystrix."
183 | []
184 | (hooke/remove-hook #'http/request ::wrap-hystrix))
185 |
186 | (defn wrap-hystrix
187 | "Middleware for adding hystrix to a clj-http client request.
188 |
189 | Alternative to `add-hook`. Do not use both `wrap-hystrix` and `add-hook`.
190 |
191 | ;; add wrap-hystrix to the middleware chain
192 | (clj-http.client/with-additional-middleware [wrap-hystrix]
193 | (clj-http.client/get ...))
194 |
195 | ;; or if you want to provide your own defaults
196 | (clj-http.client/with-additional-middleware [(partial wrap-hystrix {:hystrix/timeout-ms 6000})]
197 | (clj-http.client/get ...))
198 | "
199 | ([client] (wrap-hystrix {} client))
200 | ([defaults client]
201 | (let [wrapper (hystrix-wrapper defaults)]
202 | (fn [req]
203 | (wrapper client req)))))
204 |
--------------------------------------------------------------------------------
/test/clj_http_hystrix/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns clj-http-hystrix.core-test
2 | (:require [clj-http-hystrix.core :refer :all]
3 | [clj-http-hystrix.util :refer :all]
4 | [clj-http.client :as http]
5 | [clojure.java.io :refer [resource]]
6 | [clojure.tools.logging :refer [warn error]]
7 | [robert.hooke :as hooke]
8 | [rest-cljer.core :refer [rest-driven]]
9 | [midje.sweet :refer :all])
10 | (:import [java.net SocketTimeoutException]
11 | [java.util UUID]
12 | [clojure.lang ExceptionInfo]
13 | [org.slf4j MDC]))
14 |
15 | (def url "http://localhost:8081/")
16 |
17 | (add-hook)
18 | (fake-mdc)
19 |
20 | (defn make-hystrix-call
21 | [opts]
22 | (http/get url (merge {:hystrix/command-key (keyword (str (UUID/randomUUID)))}
23 | opts)))
24 |
25 | (fact "hystrix wrapping with fallback"
26 | (rest-driven
27 | [{:method :GET
28 | :url "/"}
29 | {:status 500}]
30 | (make-hystrix-call {:throw-exceptions false
31 | :hystrix/fallback-fn (constantly "foo")}) => "foo"))
32 |
33 | (fact "hystrix wrapping fallback not called for client-error calls - this is due to default client-error?"
34 | (rest-driven
35 | [{:method :GET
36 | :url "/"}
37 | {:status 400}]
38 | (-> (make-hystrix-call {:throw-exceptions false
39 | :hystrix/fallback-fn (constantly "foo")})
40 | :status)
41 | => 400))
42 |
43 | (fact "hystrix wrapping with fallback - preserves the MDC Values"
44 | (rest-driven
45 | [{:method :GET
46 | :url "/"}
47 | {:status 500}]
48 | (MDC/put "pickles" "preserve")
49 | (make-hystrix-call {:throw-exceptions false
50 | :hystrix/fallback-fn (fn [& z] (into {} (MDC/getCopyOfContextMap)))})
51 | => (contains {"pickles" "preserve"})))
52 |
53 | (def ^:dynamic *bindable* :unbound)
54 |
55 | (fact "hystrix wrapping with fallback - preserves dynamic bindings"
56 | (let [fallback (fn [& z] *bindable*)]
57 | (rest-driven
58 | [{:method :GET
59 | :url "/"}
60 | {:status 500}]
61 | (binding [*bindable* :bound]
62 | (make-hystrix-call {:throw-exceptions false
63 | :hystrix/fallback-fn fallback}))
64 | => :bound)))
65 |
66 | (fact "hystrix wrapping return successful call"
67 | (rest-driven
68 | [{:method :GET
69 | :url "/"}
70 | {:status 200}]
71 | (-> (make-hystrix-call {:throw-exceptions false
72 | :hystrix/fallback-fn (constantly "foo")})
73 | :status)
74 | => 200))
75 |
76 | (fact "hystrix wrapping with exceptions off"
77 | (rest-driven
78 | [{:method :GET
79 | :url "/"}
80 | {:status 500}]
81 | (-> (make-hystrix-call {:throw-exceptions false})
82 | :status)
83 | => 500))
84 |
85 | (fact "hystrix wrapping with exceptions implicitly on"
86 | (rest-driven
87 | [{:method :GET
88 | :url "/"}
89 | {:status 500}]
90 | (make-hystrix-call {})
91 | => (throws clojure.lang.ExceptionInfo "clj-http: status 500")))
92 |
93 | (fact "hystrix wrapping with exceptions explicitly on"
94 | (rest-driven
95 | [{:method :GET
96 | :url "/"}
97 | {:status 500}]
98 | (make-hystrix-call {:throw-exceptions true})
99 | => (throws clojure.lang.ExceptionInfo "clj-http: status 500")))
100 |
101 | (fact "request with no hystrix key present"
102 | (rest-driven
103 | [{:method :GET
104 | :url "/"}
105 | {:status 500}]
106 | (http/get url {})
107 | => (throws clojure.lang.ExceptionInfo "clj-http: status 500")))
108 |
109 | (fact "hystrix wrapping with sockettimeout returns 503 status"
110 | (rest-driven
111 | [{:method :GET
112 | :url "/"}
113 | {:status 200
114 | :after 500}]
115 | (let [response (make-hystrix-call {:socket-timeout 100
116 | :throw-exceptions false})]
117 | (:status response) => 503)))
118 |
119 | (fact "hystrix wrapping with timeout returns 503 status"
120 | (rest-driven
121 | [{:method :GET
122 | :url "/"}
123 | {:status 200
124 | :after 1000}]
125 | (let [response (make-hystrix-call {:hystrix/timeout-ms 100
126 | :throw-exceptions false})]
127 | (:status response) => 503)))
128 |
129 | (fact "errors will not cause circuit to break if bad-request-pred is true, with :throw-exceptions false"
130 | (rest-driven
131 | [{:method :GET
132 | :url "/"}
133 | {:status 400
134 | :times 30}
135 | {:method :GET
136 | :url "/"}
137 | {:status 200}]
138 | (let [command-key (keyword (str (UUID/randomUUID)))]
139 | (dotimes [_ 30]
140 | (http/get url {:throw-exceptions false
141 | :hystrix/command-key command-key
142 | :hystrix/bad-request-pred client-error?}) => (contains {:status 400}))
143 | (Thread/sleep 600) ;sleep to wait for Hystrix health snapshot
144 | (http/get url {:throw-exceptions false
145 | :hystrix/command-key command-key}) => (contains {:status 200}))))
146 |
147 | (fact "errors will not cause circuit to break if bad-request-pred is true, with :throw-exceptions true"
148 | (rest-driven
149 | [{:method :GET
150 | :url "/"}
151 | {:status 400
152 | :times 30}
153 | {:method :GET
154 | :url "/"}
155 | {:status 200}]
156 | (let [command-key (keyword (str (UUID/randomUUID)))]
157 | (dotimes [_ 30]
158 | (http/get url {:throw-exceptions true
159 | :hystrix/command-key command-key
160 | :hystrix/bad-request-pred client-error?}) => (throws ExceptionInfo))
161 | (Thread/sleep 600) ;sleep to wait for Hystrix health snapshot
162 | (http/get url {:throw-exceptions false
163 | :hystrix/command-key command-key}) => (contains {:status 200}))))
164 |
165 | (fact "errors will cause circuit to break if bad-request-pred is false"
166 | (rest-driven
167 | [{:method :GET
168 | :url "/"}
169 | {:status 400
170 | :times 30}]
171 | (let [command-key (keyword (str (UUID/randomUUID)))]
172 | (dotimes [_ 30]
173 | (http/get url {:throw-exceptions false
174 | :hystrix/command-key command-key
175 | :hystrix/bad-request-pred (constantly false)}) => (contains {:status 400}))
176 | (Thread/sleep 600) ;sleep to wait for Hystrix health snapshot
177 | (http/get url {:throw-exceptions false
178 | :hystrix/command-key command-key}) => (contains {:status 503}))))
179 |
180 | (fact "status-codes predicate matches only given status codes"
181 | (let [predicate (status-codes 100 200 300)]
182 | (predicate {} {:status 100}) => true
183 | (predicate {} {:status 200}) => true
184 | (predicate {} {:status 300}) => true
185 | (predicate {} {:status 101}) => false
186 | (predicate {} {:status 202}) => false
187 | (predicate {} {:status 299}) => false))
188 |
189 | (defn get-hooks []
190 | (some-> http/request meta :robert.hooke/hooks deref))
191 |
192 | (fact "add-hook can be safely called more than once"
193 | (count (get-hooks)) => 1
194 | (contains? (get-hooks) :clj-http-hystrix.core/wrap-hystrix) => true
195 | ;call add-hook a few more times and ensure only one hook is present
196 | (add-hook), (add-hook)
197 | (count (get-hooks)) => 1
198 | (contains? (get-hooks) :clj-http-hystrix.core/wrap-hystrix) => true)
199 |
200 | (fact "remove-hook removes clj-http-hystrix hook"
201 | (count (get-hooks)) => 1
202 | (contains? (get-hooks) :clj-http-hystrix.core/wrap-hystrix) => true
203 | (remove-hook)
204 | (get-hooks) => nil
205 | ;can be called more than once
206 | (remove-hook), (remove-hook)
207 | (get-hooks) => nil
208 | ;restore hook for additional testing
209 | (add-hook))
210 |
211 | (fact "add-hook with user-defaults will override base configuration, but not call configuration"
212 | (rest-driven
213 | [{:method :GET
214 | :url "/"}
215 | {:status 500
216 | :times 3}]
217 | (make-hystrix-call {})
218 | => (throws clojure.lang.ExceptionInfo "clj-http: status 500")
219 | ;set custom default for fallback-fn
220 | (remove-hook)
221 | (add-hook {:hystrix/fallback-fn (constantly "bar")})
222 | (make-hystrix-call {}) => "bar"
223 | (make-hystrix-call {:hystrix/fallback-fn (constantly "baz")}) => "baz")
224 | (remove-hook)
225 | (add-hook))
226 |
227 | (fact "wrap-hystrix enables clj-http-hystrix to be incorporated as a middleware"
228 | (remove-hook)
229 | ;verify hystrix is enabled by exceeding the default timeout (1000 ms)
230 | (http/with-additional-middleware [wrap-hystrix]
231 | (rest-driven
232 | [{:method :GET
233 | :url "/"}
234 | {:status 200
235 | :after 1500}]
236 | (make-hystrix-call {})
237 | => (throws clojure.lang.ExceptionInfo "clj-http: status 503")))
238 |
239 | ;verify custom defaults are supported
240 | (http/with-additional-middleware
241 | [(partial wrap-hystrix {:hystrix/fallback-fn (constantly {:status 404})})]
242 | (rest-driven
243 | [{:method :GET
244 | :url "/"}
245 | {:status 500}]
246 | (make-hystrix-call {})
247 | => (throws clojure.lang.ExceptionInfo "clj-http: status 404"))))
248 |
--------------------------------------------------------------------------------
/test/clj_http_hystrix/util.clj:
--------------------------------------------------------------------------------
1 | (ns clj-http-hystrix.util
2 | (:import [org.slf4j MDC]
3 | [org.slf4j.spi MDCAdapter]))
4 |
5 | (defn fake-mdc []
6 | (let [mdc (atom {})
7 | mdca (reify MDCAdapter
8 | (clear [_]
9 | (reset! mdc {}))
10 | (get [_ s]
11 | (@mdc s))
12 | (getCopyOfContextMap [_]
13 | @mdc)
14 | (put [_ k v]
15 | (swap! mdc assoc k v))
16 | (remove [_ k]
17 | (swap! mdc dissoc k))
18 | (setContextMap [_ m]
19 | (reset! mdc (into {} m))))]
20 | (doto (.getDeclaredField MDC "mdcAdapter") (.setAccessible true) (.set nil mdca))))
21 |
--------------------------------------------------------------------------------