The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .gitignore
├── .travis.yml
├── AUTHORS.md
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── backoff.go
├── backoff_test.go
├── client.go
├── doc
    └── heimdall-logo.png
├── examples
    └── client.go
├── go.mod
├── go.sum
├── httpclient
    ├── client.go
    ├── client_test.go
    ├── options.go
    ├── options_test.go
    └── plugin_mock.go
├── hystrix
    ├── hystrix_client.go
    ├── hystrix_client_test.go
    ├── options.go
    └── options_test.go
├── plugin.go
├── plugins
    └── request_logger.go
├── retry.go
└── retry_test.go


/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /out/
3 | *.log
4 | .idea/
5 | *.out
6 | .DS_Store
7 | 


--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
 1 | language: go
 2 | 
 3 | go:
 4 |  - 1.14.x
 5 | 
 6 | install:
 7 |   - make setup
 8 | 
 9 | script:
10 |   - make
11 | 


--------------------------------------------------------------------------------
/AUTHORS.md:
--------------------------------------------------------------------------------
1 | # Heimdall - Authors
2 | 
3 | For people who've contributed to [Heimdall](https://github.com/gojek/heimdall),
4 | _please checkout [Contributors Graphs](https://github.com/gojek/heimdall/graphs/contributors) 
5 | on [GO-JEK Tech's GitHub](https://github.com/gojek)._
6 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Heimdall - Changelog
2 | 
3 | ## v0.0.1 (2018-JAN-19)
4 | 
5 | - initial fork commit
6 | - open-source stepping stones
7 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # Heimdall - Contributing
 2 | 
 3 | Heimdall `github.com/gojek/heimdall` is an open-source project. 
 4 | It is licensed using the [Apache License 2.0][1]. 
 5 | We appreciate pull requests; here are our guidelines:
 6 | 
 7 | 1.  [File an issue][2] 
 8 |     (if there isn't one already). If your patch
 9 |     is going to be large it might be a good idea to get the
10 |     discussion started early.  We are happy to discuss it in a
11 |     new issue beforehand, and you can always email
12 |     <tech+heimdall@go-jek.com> about future work.
13 | 
14 | 2.  Please use [Effective Go Community Guidelines][3].
15 | 
16 | 3.  We ask that you squash all the commits together before
17 |     pushing and that your commit message references the bug.
18 | 
19 | ## Issue Reporting
20 | - Check that the issue has not already been reported.
21 | - Be clear, concise and precise in your description of the problem.
22 | - Open an issue with a descriptive title and a summary in grammatically correct,
23 |   complete sentences.
24 | - Include any relevant code to the issue summary.
25 | 
26 | ## Pull Requests
27 | - Please read this [how to GitHub][4] blog post.
28 | - Use a topic branch to easily amend a pull request later, if necessary.
29 | - Write [good commit messages][5].
30 | - Use the same coding conventions as the rest of the project.
31 | - Open a [pull request][6] that relates to *only* one subject with a clear title
32 |   and description in grammatically correct, complete sentences.
33 | 
34 | Much Thanks! ❤❤❤
35 | 
36 | GO-JEK Tech
37 | 
38 | [1]: http://www.apache.org/licenses/LICENSE-2.0
39 | [2]: https://github.com/gojek/heimdall/issues
40 | [3]: https://golang.org/doc/effective_go.html
41 | [4]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request
42 | [5]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
43 | [6]: https://help.github.com/articles/using-pull-requests
44 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
  1 | 
  2 |                                  Apache License
  3 |                            Version 2.0, January 2004
  4 |                         http://www.apache.org/licenses/
  5 | 
  6 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  7 | 
  8 |    1. Definitions.
  9 | 
 10 |       "License" shall mean the terms and conditions for use, reproduction,
 11 |       and distribution as defined by Sections 1 through 9 of this document.
 12 | 
 13 |       "Licensor" shall mean the copyright owner or entity authorized by
 14 |       the copyright owner that is granting the License.
 15 | 
 16 |       "Legal Entity" shall mean the union of the acting entity and all
 17 |       other entities that control, are controlled by, or are under common
 18 |       control with that entity. For the purposes of this definition,
 19 |       "control" means (i) the power, direct or indirect, to cause the
 20 |       direction or management of such entity, whether by contract or
 21 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 22 |       outstanding shares, or (iii) beneficial ownership of such entity.
 23 | 
 24 |       "You" (or "Your") shall mean an individual or Legal Entity
 25 |       exercising permissions granted by this License.
 26 | 
 27 |       "Source" form shall mean the preferred form for making modifications,
 28 |       including but not limited to software source code, documentation
 29 |       source, and configuration files.
 30 | 
 31 |       "Object" form shall mean any form resulting from mechanical
 32 |       transformation or translation of a Source form, including but
 33 |       not limited to compiled object code, generated documentation,
 34 |       and conversions to other media types.
 35 | 
 36 |       "Work" shall mean the work of authorship, whether in Source or
 37 |       Object form, made available under the License, as indicated by a
 38 |       copyright notice that is included in or attached to the work
 39 |       (an example is provided in the Appendix below).
 40 | 
 41 |       "Derivative Works" shall mean any work, whether in Source or Object
 42 |       form, that is based on (or derived from) the Work and for which the
 43 |       editorial revisions, annotations, elaborations, or other modifications
 44 |       represent, as a whole, an original work of authorship. For the purposes
 45 |       of this License, Derivative Works shall not include works that remain
 46 |       separable from, or merely link (or bind by name) to the interfaces of,
 47 |       the Work and Derivative Works thereof.
 48 | 
 49 |       "Contribution" shall mean any work of authorship, including
 50 |       the original version of the Work and any modifications or additions
 51 |       to that Work or Derivative Works thereof, that is intentionally
 52 |       submitted to Licensor for inclusion in the Work by the copyright owner
 53 |       or by an individual or Legal Entity authorized to submit on behalf of
 54 |       the copyright owner. For the purposes of this definition, "submitted"
 55 |       means any form of electronic, verbal, or written communication sent
 56 |       to the Licensor or its representatives, including but not limited to
 57 |       communication on electronic mailing lists, source code control systems,
 58 |       and issue tracking systems that are managed by, or on behalf of, the
 59 |       Licensor for the purpose of discussing and improving the Work, but
 60 |       excluding communication that is conspicuously marked or otherwise
 61 |       designated in writing by the copyright owner as "Not a Contribution."
 62 | 
 63 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 64 |       on behalf of whom a Contribution has been received by Licensor and
 65 |       subsequently incorporated within the Work.
 66 | 
 67 |    2. Grant of Copyright License. Subject to the terms and conditions of
 68 |       this License, each Contributor hereby grants to You a perpetual,
 69 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 70 |       copyright license to reproduce, prepare Derivative Works of,
 71 |       publicly display, publicly perform, sublicense, and distribute the
 72 |       Work and such Derivative Works in Source or Object form.
 73 | 
 74 |    3. Grant of Patent License. Subject to the terms and conditions of
 75 |       this License, each Contributor hereby grants to You a perpetual,
 76 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 77 |       (except as stated in this section) patent license to make, have made,
 78 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 79 |       where such license applies only to those patent claims licensable
 80 |       by such Contributor that are necessarily infringed by their
 81 |       Contribution(s) alone or by combination of their Contribution(s)
 82 |       with the Work to which such Contribution(s) was submitted. If You
 83 |       institute patent litigation against any entity (including a
 84 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 85 |       or a Contribution incorporated within the Work constitutes direct
 86 |       or contributory patent infringement, then any patent licenses
 87 |       granted to You under this License for that Work shall terminate
 88 |       as of the date such litigation is filed.
 89 | 
 90 |    4. Redistribution. You may reproduce and distribute copies of the
 91 |       Work or Derivative Works thereof in any medium, with or without
 92 |       modifications, and in Source or Object form, provided that You
 93 |       meet the following conditions:
 94 | 
 95 |       (a) You must give any other recipients of the Work or
 96 |           Derivative Works a copy of this License; and
 97 | 
 98 |       (b) You must cause any modified files to carry prominent notices
 99 |           stating that You changed the files; and
100 | 
101 |       (c) You must retain, in the Source form of any Derivative Works
102 |           that You distribute, all copyright, patent, trademark, and
103 |           attribution notices from the Source form of the Work,
104 |           excluding those notices that do not pertain to any part of
105 |           the Derivative Works; and
106 | 
107 |       (d) If the Work includes a "NOTICE" text file as part of its
108 |           distribution, then any Derivative Works that You distribute must
109 |           include a readable copy of the attribution notices contained
110 |           within such NOTICE file, excluding those notices that do not
111 |           pertain to any part of the Derivative Works, in at least one
112 |           of the following places: within a NOTICE text file distributed
113 |           as part of the Derivative Works; within the Source form or
114 |           documentation, if provided along with the Derivative Works; or,
115 |           within a display generated by the Derivative Works, if and
116 |           wherever such third-party notices normally appear. The contents
117 |           of the NOTICE file are for informational purposes only and
118 |           do not modify the License. You may add Your own attribution
119 |           notices within Derivative Works that You distribute, alongside
120 |           or as an addendum to the NOTICE text from the Work, provided
121 |           that such additional attribution notices cannot be construed
122 |           as modifying the License.
123 | 
124 |       You may add Your own copyright statement to Your modifications and
125 |       may provide additional or different license terms and conditions
126 |       for use, reproduction, or distribution of Your modifications, or
127 |       for any such Derivative Works as a whole, provided Your use,
128 |       reproduction, and distribution of the Work otherwise complies with
129 |       the conditions stated in this License.
130 | 
131 |    5. Submission of Contributions. Unless You explicitly state otherwise,
132 |       any Contribution intentionally submitted for inclusion in the Work
133 |       by You to the Licensor shall be under the terms and conditions of
134 |       this License, without any additional terms or conditions.
135 |       Notwithstanding the above, nothing herein shall supersede or modify
136 |       the terms of any separate license agreement you may have executed
137 |       with Licensor regarding such Contributions.
138 | 
139 |    6. Trademarks. This License does not grant permission to use the trade
140 |       names, trademarks, service marks, or product names of the Licensor,
141 |       except as required for reasonable and customary use in describing the
142 |       origin of the Work and reproducing the content of the NOTICE file.
143 | 
144 |    7. Disclaimer of Warranty. Unless required by applicable law or
145 |       agreed to in writing, Licensor provides the Work (and each
146 |       Contributor provides its Contributions) on an "AS IS" BASIS,
147 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 |       implied, including, without limitation, any warranties or conditions
149 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 |       PARTICULAR PURPOSE. You are solely responsible for determining the
151 |       appropriateness of using or redistributing the Work and assume any
152 |       risks associated with Your exercise of permissions under this License.
153 | 
154 |    8. Limitation of Liability. In no event and under no legal theory,
155 |       whether in tort (including negligence), contract, or otherwise,
156 |       unless required by applicable law (such as deliberate and grossly
157 |       negligent acts) or agreed to in writing, shall any Contributor be
158 |       liable to You for damages, including any direct, indirect, special,
159 |       incidental, or consequential damages of any character arising as a
160 |       result of this License or out of the use or inability to use the
161 |       Work (including but not limited to damages for loss of goodwill,
162 |       work stoppage, computer failure or malfunction, or any and all
163 |       other commercial damages or losses), even if such Contributor
164 |       has been advised of the possibility of such damages.
165 | 
166 |    9. Accepting Warranty or Additional Liability. While redistributing
167 |       the Work or Derivative Works thereof, You may choose to offer,
168 |       and charge a fee for, acceptance of support, warranty, indemnity,
169 |       or other liability obligations and/or rights consistent with this
170 |       License. However, in accepting such obligations, You may act only
171 |       on Your own behalf and on Your sole responsibility, not on behalf
172 |       of any other Contributor, and only if You agree to indemnify,
173 |       defend, and hold each Contributor harmless for any liability
174 |       incurred by, or claims asserted against, such Contributor by reason
175 |       of your accepting any such warranty or additional liability.
176 | 
177 |    END OF TERMS AND CONDITIONS
178 | 
179 |    APPENDIX: How to apply the Apache License to your work.
180 | 
181 |       To apply the Apache License to your work, attach the following
182 |       boilerplate notice, with the fields enclosed by brackets "[]"
183 |       replaced with your own identifying information. (Don't include
184 |       the brackets!)  The text should be enclosed in the appropriate
185 |       comment syntax for the file format. We also recommend that a
186 |       file or class name and description of purpose be included on the
187 |       same "printed page" as the copyright notice for easier
188 |       identification within third-party archives.
189 | 
190 |    Copyright [yyyy] [name of copyright owner]
191 | 
192 |    Licensed under the Apache License, Version 2.0 (the "License");
193 |    you may not use this file except in compliance with the License.
194 |    You may obtain a copy of the License at
195 | 
196 |        http://www.apache.org/licenses/LICENSE-2.0
197 | 
198 |    Unless required by applicable law or agreed to in writing, software
199 |    distributed under the License is distributed on an "AS IS" BASIS,
200 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 |    See the License for the specific language governing permissions and
202 |    limitations under the License.
203 | 


--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
 1 | .PHONYthub.com/mattn/goveralls: all
 2 | all: build test coverage
 3 | 
 4 | ALL_PACKAGES=$(shell go list ./... | grep -v "vendor")
 5 | 
 6 | setup:
 7 | 	mkdir -p $(GOPATH)/bin
 8 | 	go get -u golang.org/x/lint/golint
 9 | 	go get github.com/mattn/goveralls
10 | 
11 | compile:
12 | 	mkdir -p out/
13 | 	go build -race ./...
14 | 
15 | build: compile fmt vet lint
16 | 
17 | fmt:
18 | 	go fmt ./...
19 | 
20 | vet:
21 | 	go vet ./...
22 | 
23 | lint:
24 | 	golint -set_exit_status $(ALL_PACKAGES)
25 | 
26 | test: fmt vet build
27 | 	ENVIRONMENT=test go test -race ./...
28 | 
29 | coverage:
30 | 	ENVIRONMENT=test goveralls -service=travis-ci
31 | 
32 | test-cover-html:
33 | 	@echo "mode: count" > coverage-all.out
34 | 
35 | 	$(foreach pkg, $(ALL_PACKAGES),\
36 | 	ENVIRONMENT=test go test -coverprofile=coverage.out -covermode=count $(pkg);\
37 | 	tail -n +2 coverage.out >> coverage-all.out;)
38 | 	go tool cover -html=coverage-all.out -o out/coverage.html
39 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # Heimdall
  2 | 
  3 | <p align="center"><img src="doc/heimdall-logo.png" width="360"></p>
  4 | <p align="center">
  5 |   <a href="https://travis-ci.com/gojek/heimdall"><img src="https://travis-ci.com/gojek/heimdall.svg?branch=master" alt="Build Status"></img></a>
  6 |   <a href="https://goreportcard.com/report/github.com/gojek/heimdall"><img src="https://goreportcard.com/badge/github.com/gojek/heimdall"></img></a>
  7 |   <a href="https://golangci.com"><img src="https://golangci.com/badges/github.com/gojek/heimdall.svg"></img></a>
  8 |   <a href="https://coveralls.io/github/gojek/heimdall?branch=master"><img src="https://coveralls.io/repos/github/gojek/heimdall/badge.svg?branch=master"></img></a>
  9 | </p>
 10 | 
 11 | * [Description](#description)
 12 | * [Installation](#installation)
 13 | * [Usage](#usage)
 14 |   + [Making a simple `GET` request](#making-a-simple-get-request)
 15 |   + [Creating a hystrix-like circuit breaker](#creating-a-hystrix-like-circuit-breaker)
 16 |   + [Creating a hystrix-like circuit breaker with fallbacks](#creating-a-hystrix-like-circuit-breaker-with-fallbacks)
 17 |   + [Creating an HTTP client with a retry mechanism](#creating-an-http-client-with-a-retry-mechanism)
 18 |   + [Custom retry mechanisms](#custom-retry-mechanisms)
 19 |   + [Custom HTTP clients](#custom-http-clients)
 20 | * [Plugins](#plugins)
 21 | * [Documentation](#documentation)
 22 | * [FAQ](#faq)
 23 | * [License](#license)
 24 | 
 25 | ## Description
 26 | 
 27 | Heimdall is an HTTP client that helps your application make a large number of requests, at scale. With Heimdall, you can:
 28 | - Use a [hystrix-like](https://github.com/afex/hystrix-go) circuit breaker to control failing requests
 29 | - Add synchronous in-memory retries to each request, with the option of setting your own retrier strategy
 30 | - Create clients with different timeouts for every request
 31 | 
 32 | All HTTP methods are exposed as a fluent interface.
 33 | 
 34 | ## Installation
 35 | ```
 36 | go get -u github.com/gojek/heimdall/v7
 37 | ```
 38 | 
 39 | ## Usage
 40 | 
 41 | ### Importing the package
 42 | 
 43 | This package can be used by adding the following import statement to your `.go` files.
 44 | 
 45 | ```go
 46 | import "github.com/gojek/heimdall/v7/httpclient" 
 47 | ```
 48 | 
 49 | ### Making a simple `GET` request
 50 | The below example will print the contents of the google home page:
 51 | 
 52 | ```go
 53 | // Create a new HTTP client with a default timeout
 54 | timeout := 1000 * time.Millisecond
 55 | client := httpclient.NewClient(httpclient.WithHTTPTimeout(timeout))
 56 | 
 57 | // Use the clients GET method to create and execute the request
 58 | res, err := client.Get("http://google.com", nil)
 59 | if err != nil{
 60 | 	panic(err)
 61 | }
 62 | 
 63 | // Heimdall returns the standard *http.Response object
 64 | body, err := ioutil.ReadAll(res.Body)
 65 | fmt.Println(string(body))
 66 | ```
 67 | 
 68 | You can also use the `*http.Request` object with the `http.Do` interface :
 69 | 
 70 | ```go
 71 | timeout := 1000 * time.Millisecond
 72 | client := httpclient.NewClient(httpclient.WithHTTPTimeout(timeout))
 73 | 
 74 | // Create an http.Request instance
 75 | req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
 76 | // Call the `Do` method, which has a similar interface to the `http.Do` method
 77 | res, err := client.Do(req)
 78 | if err != nil {
 79 | 	panic(err)
 80 | }
 81 | 
 82 | body, err := ioutil.ReadAll(res.Body)
 83 | fmt.Println(string(body))
 84 | ```
 85 | 
 86 | ### Creating a hystrix-like circuit breaker
 87 | 
 88 | To import hystrix package of heimdall.
 89 | 
 90 | ```go
 91 | import "github.com/gojek/heimdall/v7/hystrix"
 92 | ```
 93 | 
 94 | You can use the `hystrix.NewClient` function to create a client wrapped in a hystrix-like circuit breaker:
 95 | 
 96 | ```go
 97 | // Create a new hystrix-wrapped HTTP client with the command name, along with other required options
 98 | client := hystrix.NewClient(
 99 | 	hystrix.WithHTTPTimeout(10 * time.Millisecond),
100 | 	hystrix.WithCommandName("google_get_request"),
101 | 	hystrix.WithHystrixTimeout(1000 * time.Millisecond),
102 | 	hystrix.WithMaxConcurrentRequests(30),
103 | 	hystrix.WithErrorPercentThreshold(20),
104 | 	hystrix.WithStatsDCollector("localhost:8125", "myapp.hystrix"),
105 | )
106 | 
107 | // The rest is the same as the previous example
108 | ```
109 | 
110 | In the above example, there are two timeout values used: one for the hystrix configuration, and one for the HTTP client configuration. The former determines the time at which hystrix should register an error, while the latter determines when the client itself should return a timeout error. Unless you have any special requirements, both of these would have the same values.
111 | 
112 | You can choose to export hystrix metrics to a statsD collector with the `hystrix.WithStatsDCollector(<statsd addr>, <metrics-prefix>)` option when initializing the client as shown above.
113 | 
114 | ### Creating a hystrix-like circuit breaker with fallbacks
115 | 
116 | You can use the `hystrix.NewClient` function to create a client wrapped in a hystrix-like circuit breaker by passing in your own custom fallbacks:
117 | 
118 | The fallback function will trigger when your code returns an error, or whenever it is unable to complete based on a variety of [health checks](https://github.com/Netflix/Hystrix/wiki/How-it-Works).
119 | 
120 | **How your fallback function should look like**
121 | you should pass in a function whose signature looks like following
122 | 
123 | ```go
124 | func(err error) error {
125 |     // your logic for handling the error/outage condition
126 |     return err
127 | }
128 | ```
129 | 
130 | 
131 | **Example**
132 | 
133 | ```go
134 | // Create a new fallback function
135 | fallbackFn := func(err error) error {
136 |     _, err := http.Post("post_to_channel_two")
137 |     return err
138 | }
139 | 
140 | timeout := 10 * time.Millisecond
141 | 
142 | // Create a new hystrix-wrapped HTTP client with the fallbackFunc as fall-back function
143 | client := hystrix.NewClient(
144 | 	hystrix.WithHTTPTimeout(timeout),
145 | 	hystrix.WithCommandName("MyCommand"),
146 | 	hystrix.WithHystrixTimeout(1100 * time.Millisecond),
147 | 	hystrix.WithMaxConcurrentRequests(100),
148 | 	hystrix.WithErrorPercentThreshold(20),
149 | 	hystrix.WithSleepWindow(10),
150 | 	hystrix.WithRequestVolumeThreshold(10),
151 | 	hystrix.WithFallbackFunc(fallbackFn),
152 | })
153 | 
154 | // The rest is the same as the previous example
155 | ```
156 | 
157 | In the above example, the `fallbackFunc` is a function which posts to channel two in case posting to channel one fails.
158 | 
159 | ### Creating an HTTP client with a retry mechanism
160 | 
161 | ```go
162 | // First set a backoff mechanism. Constant backoff increases the backoff at a constant rate
163 | backoffInterval := 2 * time.Millisecond
164 | // Define a maximum jitter interval. It must be more than 1*time.Millisecond
165 | maximumJitterInterval := 5 * time.Millisecond
166 | 
167 | backoff := heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval)
168 | 
169 | // Create a new retry mechanism with the backoff
170 | retrier := heimdall.NewRetrier(backoff)
171 | 
172 | timeout := 1000 * time.Millisecond
173 | // Create a new client, sets the retry mechanism, and the number of times you would like to retry
174 | client := httpclient.NewClient(
175 | 	httpclient.WithHTTPTimeout(timeout),
176 | 	httpclient.WithRetrier(retrier),
177 | 	httpclient.WithRetryCount(4),
178 | )
179 | 
180 | // The rest is the same as the first example
181 | ```
182 | Or create client with exponential backoff
183 | 
184 | ```go
185 | // First set a backoff mechanism. Exponential Backoff increases the backoff at a exponential rate
186 | 
187 | initalTimeout := 2*time.Millisecond            // Inital timeout
188 | maxTimeout := 9*time.Millisecond               // Max time out
189 | exponentFactor := 2                            // Multiplier
190 | maximumJitterInterval := 2*time.Millisecond    // Max jitter interval. It must be more than 1*time.Millisecond
191 | 
192 | backoff := heimdall.NewExponentialBackoff(initalTimeout, maxTimeout, exponentFactor, maximumJitterInterval)
193 | 
194 | // Create a new retry mechanism with the backoff
195 | retrier := heimdall.NewRetrier(backoff)
196 | 
197 | timeout := 1000 * time.Millisecond
198 | // Create a new client, sets the retry mechanism, and the number of times you would like to retry
199 | client := httpclient.NewClient(
200 | 	httpclient.WithHTTPTimeout(timeout),
201 | 	httpclient.WithRetrier(retrier),
202 | 	httpclient.WithRetryCount(4),
203 | )
204 | 
205 | // The rest is the same as the first example
206 | ```
207 | 
208 | This will create an HTTP client which will retry every `500` milliseconds incase the request fails. The library also comes with an [Exponential Backoff](https://pkg.go.dev/github.com/gojek/heimdall#NewExponentialBackoff)
209 | 
210 | ### Custom retry mechanisms
211 | 
212 | Heimdall supports custom retry strategies. To do this, you will have to implement the `Backoff` interface:
213 | 
214 | ```go
215 | type Backoff interface {
216 | 	Next(retry int) time.Duration
217 | }
218 | ```
219 | 
220 | Let's see an example of creating a client with a linearly increasing backoff time:
221 | 
222 | First, create the backoff mechanism:
223 | 
224 | ```go
225 | type linearBackoff struct {
226 | 	backoffInterval int
227 | }
228 | 
229 | func (lb *linearBackoff) Next(retry int) time.Duration{
230 | 	if retry <= 0 {
231 | 		return 0 * time.Millisecond
232 | 	}
233 | 	return time.Duration(retry * lb.backoffInterval) * time.Millisecond
234 | }
235 | ```
236 | 
237 | This will create a backoff mechanism, where the retry time will increase linearly for each retry attempt. We can use this to create the client, just like the last example:
238 | 
239 | ```go
240 | backoff := &linearBackoff{100}
241 | retrier := heimdall.NewRetrier(backoff)
242 | 
243 | timeout := 1000 * time.Millisecond
244 | // Create a new client, sets the retry mechanism, and the number of times you would like to retry
245 | client := httpclient.NewClient(
246 | 	httpclient.WithHTTPTimeout(timeout),
247 | 	httpclient.WithRetrier(retrier),
248 | 	httpclient.WithRetryCount(4),
249 | )
250 | 
251 | // The rest is the same as the first example
252 | ```
253 | 
254 | Heimdall also allows you to simply pass a function that returns the retry timeout. This can be used to create the client, like:
255 | ```go
256 | linearRetrier := NewRetrierFunc(func(retry int) time.Duration {
257 | 	if retry <= 0 {
258 | 		return 0 * time.Millisecond
259 | 	}
260 | 	return time.Duration(retry) * time.Millisecond
261 | })
262 | 
263 | timeout := 1000 * time.Millisecond
264 | client := httpclient.NewClient(
265 | 	httpclient.WithHTTPTimeout(timeout),
266 | 	httpclient.WithRetrier(linearRetrier),
267 | 	httpclient.WithRetryCount(4),
268 | )
269 | ```
270 | 
271 | ### Custom HTTP clients
272 | 
273 | Heimdall supports custom HTTP clients. This is useful if you are using a client imported from another library and/or wish to implement custom logging, cookies, headers etc for each request that you make with your client.
274 | 
275 | Under the hood, the `httpClient` struct now accepts `Doer`, which is the standard interface implemented by HTTP clients (including the standard library's `net/*http.Client`)
276 | 
277 | Let's say we wish to add authorization headers to all our requests.
278 | 
279 | We can define our client `myHTTPClient`
280 | 
281 | ```go
282 | type myHTTPClient struct {
283 | 	client http.Client
284 | }
285 | 
286 | func (c *myHTTPClient) Do(request *http.Request) (*http.Response, error) {
287 | 	request.SetBasicAuth("username", "passwd")
288 | 	return c.client.Do(request)
289 | }
290 | ```
291 | 
292 | And set this with `httpclient.NewClient(httpclient.WithHTTPClient(&myHTTPClient{client: http.DefaultClient}))`
293 | 
294 | Now, each sent request will have the `Authorization` header to use HTTP basic authentication with the provided username and password.
295 | 
296 | This can be done for the hystrix client as well
297 | 
298 | ```go
299 | client := httpclient.NewClient(
300 | 	httpclient.WithHTTPClient(&myHTTPClient{
301 | 		client: http.Client{Timeout: 25 * time.Millisecond},
302 | 	}),
303 | )
304 | 
305 | // The rest is the same as the first example
306 | ```
307 | 
308 | ## Plugins
309 | 
310 | To add a plugin to an existing client, use the `AddPlugin` method of the client. 
311 | 
312 | An example, with the [request logger plugin](/plugins/request_logger.go):
313 | 
314 | ```go
315 | // import "github.com/gojek/heimdall/v7/plugins"
316 | 
317 | client := heimdall.NewHTTPClient(timeout)
318 | requestLogger := plugins.NewRequestLogger(nil, nil)
319 | client.AddPlugin(requestLogger)
320 | // use the client as before
321 | 
322 | req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
323 | res, err := client.Do(req)
324 | if err != nil {
325 | 	panic(err)
326 | }
327 | // This will log:
328 | //23/Jun/2018 12:48:04 GET http://google.com 200 [412ms]
329 | // to STDOUT
330 | ```
331 | 
332 | A plugin is an interface whose methods get called during key events in a requests lifecycle:
333 | 
334 | - `OnRequestStart` is called just before the request is made
335 | - `OnRequestEnd` is called once the request has successfully executed
336 | - `OnError` is called is the request failed
337 | 
338 | Each method is called with the request object as an argument, with `OnRequestEnd`, and `OnError` additionally being called with the response and error instances respectively.
339 | For a simple example on how to write plugins, look at the [request logger plugin](/plugins/request_logger.go).
340 | 
341 | ## Documentation
342 | 
343 | Further documentation can be found on [pkg.go.dev](https://pkg.go.dev/github.com/gojek/heimdall/v7)
344 | 
345 | ## FAQ
346 | 
347 | **Can I replace the standard Go HTTP client with Heimdall?**
348 | 
349 | Yes, you can. Heimdall implements the standard [HTTP Do](https://golang.org/pkg/net/http/#Client.Do) method, along with [useful wrapper methods](https://golang.org/pkg/net/http/#Client.Do) that provide all the functionality that a regular Go HTTP client provides.
350 | 
351 | ---
352 | 
353 | **When should I use Heimdall?**
354 | 
355 | If you are making a large number of HTTP requests, or if you make requests among multiple distributed nodes, and wish to make your systems more fault tolerant, then Heimdall was made for you.
356 | 
357 | Heimdall makes use of [multiple mechanisms](https://medium.com/@sohamkamani/how-to-handle-microservice-communication-at-scale-a6fb0ee0ed7) to make HTTP requests more fault tolerant:
358 | 1. Retries - If a request fails, Heimdall retries behind the scenes, and returns the result if one of the retries are successful.
359 | 2. Circuit breaking - If Heimdall detects that too many of your requests are failing, or that the number of requests sent are above a configured threshold, then it "opens the circuit" for a short period of time, which prevents any more requests from being made. _This gives your downstream systems time to recover._
360 | 
361 | ---
362 | 
363 | **So does this mean that I shouldn't use Heimdall for small scale applications?**
364 | 
365 | Although Heimdall was made keeping large scale systems in mind, it's interface is simple enough to be used for any type of systems. In fact, we use it for our pet projects as well. Even if you don't require retries or circuit breaking features, the [simpler HTTP client](https://github.com/gojek/heimdall#making-a-simple-get-request) provides sensible defaults with a simpler interface, and can be upgraded easily should the need arise.
366 | 
367 | ---
368 | 
369 | **Can I contribute to make Heimdall better?**
370 | 
371 | [Please do!](https://github.com/gojek/heimdall/blob/master/CONTRIBUTING.md) We are looking for any kind of contribution to improve Heimdalls core funtionality and documentation. When in doubt, make a PR!
372 | 
373 | ## License
374 | 
375 | ```
376 | Copyright 2018-2020, GO-JEK Tech (http://gojek.tech)
377 | 
378 | Licensed under the Apache License, Version 2.0 (the "License");
379 | you may not use this file except in compliance with the License.
380 | You may obtain a copy of the License at
381 | 
382 |     http://www.apache.org/licenses/LICENSE-2.0
383 | 
384 | Unless required by applicable law or agreed to in writing, software
385 | distributed under the License is distributed on an "AS IS" BASIS,
386 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
387 | See the License for the specific language governing permissions and
388 | limitations under the License.
389 | ```
390 | 


--------------------------------------------------------------------------------
/backoff.go:
--------------------------------------------------------------------------------
 1 | package heimdall
 2 | 
 3 | import (
 4 | 	"math"
 5 | 	"math/rand"
 6 | 	"time"
 7 | )
 8 | 
 9 | // Backoff interface defines contract for backoff strategies
10 | type Backoff interface {
11 | 	Next(retry int) time.Duration
12 | }
13 | 
14 | type constantBackoff struct {
15 | 	backoffInterval       int64
16 | 	maximumJitterInterval int64
17 | }
18 | 
19 | func init() {
20 | 	rand.Seed(time.Now().UnixNano())
21 | }
22 | 
23 | // NewConstantBackoff returns an instance of ConstantBackoff
24 | func NewConstantBackoff(backoffInterval, maximumJitterInterval time.Duration) Backoff {
25 | 	// protect against panic when generating random jitter
26 | 	if maximumJitterInterval < 0 {
27 | 		maximumJitterInterval = 0
28 | 	}
29 | 
30 | 	return &constantBackoff{
31 | 		backoffInterval:       int64(backoffInterval / time.Millisecond),
32 | 		maximumJitterInterval: int64(maximumJitterInterval / time.Millisecond),
33 | 	}
34 | }
35 | 
36 | // Next returns next time for retrying operation with constant strategy
37 | func (cb *constantBackoff) Next(retry int) time.Duration {
38 | 	return (time.Duration(cb.backoffInterval) * time.Millisecond) + (time.Duration(rand.Int63n(cb.maximumJitterInterval+1)) * time.Millisecond)
39 | }
40 | 
41 | type exponentialBackoff struct {
42 | 	exponentFactor        float64
43 | 	initialTimeout        float64
44 | 	maxTimeout            float64
45 | 	maximumJitterInterval int64
46 | }
47 | 
48 | // NewExponentialBackoff returns an instance of ExponentialBackoff
49 | func NewExponentialBackoff(initialTimeout, maxTimeout time.Duration, exponentFactor float64, maximumJitterInterval time.Duration) Backoff {
50 | 	// protect against panic when generating random jitter
51 | 	if maximumJitterInterval < 0 {
52 | 		maximumJitterInterval = 0
53 | 	}
54 | 
55 | 	return &exponentialBackoff{
56 | 		exponentFactor:        exponentFactor,
57 | 		initialTimeout:        float64(initialTimeout / time.Millisecond),
58 | 		maxTimeout:            float64(maxTimeout / time.Millisecond),
59 | 		maximumJitterInterval: int64(maximumJitterInterval / time.Millisecond),
60 | 	}
61 | }
62 | 
63 | // Next returns next time for retrying operation with exponential strategy
64 | func (eb *exponentialBackoff) Next(retry int) time.Duration {
65 | 	if retry < 0 {
66 | 		retry = 0
67 | 	}
68 | 	return time.Duration(math.Min(eb.initialTimeout*math.Pow(eb.exponentFactor, float64(retry)), eb.maxTimeout)+float64(rand.Int63n(eb.maximumJitterInterval+1))) * time.Millisecond
69 | }
70 | 


--------------------------------------------------------------------------------
/backoff_test.go:
--------------------------------------------------------------------------------
  1 | package heimdall
  2 | 
  3 | import (
  4 | 	"testing"
  5 | 	"time"
  6 | 
  7 | 	"github.com/stretchr/testify/assert"
  8 | )
  9 | 
 10 | func TestExponentialBackoffNextTime(t *testing.T) {
 11 | 	exponentialBackoff := NewExponentialBackoff(100*time.Millisecond, 1000*time.Millisecond, 2.0, 0*time.Millisecond)
 12 | 
 13 | 	assert.Equal(t, 100*time.Millisecond, exponentialBackoff.Next(0))
 14 | 	assert.Equal(t, 200*time.Millisecond, exponentialBackoff.Next(1))
 15 | 	assert.Equal(t, 400*time.Millisecond, exponentialBackoff.Next(2))
 16 | 	assert.Equal(t, 800*time.Millisecond, exponentialBackoff.Next(3))
 17 | }
 18 | 
 19 | func TestExponentialBackoffWithInvalidJitter(t *testing.T) {
 20 | 	exponentialBackoff := NewExponentialBackoff(100*time.Millisecond, 1000*time.Millisecond, 2.0, -1*time.Millisecond)
 21 | 
 22 | 	assert.Equal(t, 100*time.Millisecond, exponentialBackoff.Next(0))
 23 | 	assert.Equal(t, 200*time.Millisecond, exponentialBackoff.Next(1))
 24 | 	assert.Equal(t, 400*time.Millisecond, exponentialBackoff.Next(2))
 25 | 	assert.Equal(t, 800*time.Millisecond, exponentialBackoff.Next(3))
 26 | }
 27 | 
 28 | func TestExponentialBackoffMaxTimeoutCrossed(t *testing.T) {
 29 | 	exponentialBackoff := NewExponentialBackoff(100*time.Millisecond, 1000*time.Millisecond, 2.0, 0*time.Millisecond)
 30 | 
 31 | 	assert.Equal(t, 1000*time.Millisecond, exponentialBackoff.Next(4))
 32 | }
 33 | 
 34 | func TestExponentialBackoffMaxTimeoutReached(t *testing.T) {
 35 | 	exponentialBackoff := NewExponentialBackoff(100*time.Millisecond, 1600*time.Millisecond, 2.0, 0*time.Millisecond)
 36 | 
 37 | 	assert.Equal(t, 1600*time.Millisecond, exponentialBackoff.Next(4))
 38 | }
 39 | 
 40 | func TestExponentialBackoffWhenRetryIsLessThanZero(t *testing.T) {
 41 | 	exponentialBackoff := NewExponentialBackoff(100*time.Millisecond, 1000*time.Millisecond, 2.0, 0*time.Millisecond)
 42 | 
 43 | 	assert.Equal(t, 100*time.Millisecond, exponentialBackoff.Next(-1))
 44 | }
 45 | 
 46 | func TestExponentialBackoffJitter0(t *testing.T) {
 47 | 	exponentialBackoff := NewExponentialBackoff(100*time.Millisecond, 1000*time.Millisecond, 2.0, 0*time.Millisecond)
 48 | 	for i := 0; i < 10000; i++ {
 49 | 		assert.Equal(t, 200*time.Millisecond, exponentialBackoff.Next(1))
 50 | 	}
 51 | }
 52 | 
 53 | func TestExponentialBackoffJitter1(t *testing.T) {
 54 | 	exponentialBackoff := NewExponentialBackoff(100*time.Millisecond, 1000*time.Millisecond, 2.0, 1*time.Millisecond)
 55 | 	for i := 0; i < 10000; i++ {
 56 | 		assert.True(t, 200*time.Millisecond <= exponentialBackoff.Next(1) && exponentialBackoff.Next(1) <= 201*time.Millisecond)
 57 | 	}
 58 | }
 59 | 
 60 | func TestExponentialBackoffJitter50(t *testing.T) {
 61 | 	exponentialBackoff := NewExponentialBackoff(100*time.Millisecond, 1000*time.Millisecond, 2.0, 50*time.Millisecond)
 62 | 	for i := 0; i < 10000; i++ {
 63 | 		assert.True(t, 200*time.Millisecond <= exponentialBackoff.Next(1) && exponentialBackoff.Next(1) <= 250*time.Millisecond)
 64 | 	}
 65 | }
 66 | 
 67 | func TestConstantBackoffNextTime(t *testing.T) {
 68 | 	constantBackoff := NewConstantBackoff(100*time.Millisecond, 0*time.Millisecond)
 69 | 
 70 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(0))
 71 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(1))
 72 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(2))
 73 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(3))
 74 | }
 75 | 
 76 | func TestConstantBackoffWithInvalidJitter(t *testing.T) {
 77 | 	constantBackoff := NewConstantBackoff(100*time.Millisecond, -1*time.Millisecond)
 78 | 
 79 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(0))
 80 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(1))
 81 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(2))
 82 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(3))
 83 | }
 84 | 
 85 | func TestConstantBackoffWhenRetryIsLessThanZero(t *testing.T) {
 86 | 	constantBackoff := NewConstantBackoff(100*time.Millisecond, 0*time.Millisecond)
 87 | 
 88 | 	assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(-1))
 89 | }
 90 | 
 91 | func TestConstantBackoffJitter0(t *testing.T) {
 92 | 	constantBackoff := NewConstantBackoff(100*time.Millisecond, 0*time.Millisecond)
 93 | 	for i := 0; i < 10000; i++ {
 94 | 		assert.Equal(t, 100*time.Millisecond, constantBackoff.Next(i))
 95 | 	}
 96 | }
 97 | 
 98 | func TestConstantBackoffJitter1(t *testing.T) {
 99 | 	constantBackoff := NewConstantBackoff(100*time.Millisecond, 1*time.Millisecond)
100 | 	for i := 0; i < 10000; i++ {
101 | 		assert.True(t, 100*time.Millisecond <= constantBackoff.Next(i) && constantBackoff.Next(1) <= 101*time.Millisecond)
102 | 	}
103 | }
104 | 
105 | func TestConstantBackoffJitter50(t *testing.T) {
106 | 	constantBackoff := NewConstantBackoff(100*time.Millisecond, 50*time.Millisecond)
107 | 	for i := 0; i < 10000; i++ {
108 | 		assert.True(t, 100*time.Millisecond <= constantBackoff.Next(i) && constantBackoff.Next(1) <= 150*time.Millisecond)
109 | 	}
110 | }
111 | 


--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
 1 | package heimdall
 2 | 
 3 | import (
 4 | 	"io"
 5 | 	"net/http"
 6 | )
 7 | 
 8 | // Doer interface has the method required to use a type as custom http client.
 9 | // The net/*http.Client type satisfies this interface.
10 | type Doer interface {
11 | 	Do(*http.Request) (*http.Response, error)
12 | }
13 | 
14 | // Client Is a generic HTTP client interface
15 | type Client interface {
16 | 	Get(url string, headers http.Header) (*http.Response, error)
17 | 	Post(url string, body io.Reader, headers http.Header) (*http.Response, error)
18 | 	Put(url string, body io.Reader, headers http.Header) (*http.Response, error)
19 | 	Patch(url string, body io.Reader, headers http.Header) (*http.Response, error)
20 | 	Delete(url string, headers http.Header) (*http.Response, error)
21 | 	Do(req *http.Request) (*http.Response, error)
22 | 	AddPlugin(p Plugin)
23 | }
24 | 


--------------------------------------------------------------------------------
/doc/heimdall-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gojek/heimdall/020e42a7b0b89433c415e4d7d26f9239fa3d1a89/doc/heimdall-logo.png


--------------------------------------------------------------------------------
/examples/client.go:
--------------------------------------------------------------------------------
  1 | package examples
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"io/ioutil"
  6 | 	"net/http"
  7 | 	"time"
  8 | 
  9 | 	"github.com/gojek/heimdall/v7"
 10 | 	"github.com/gojek/heimdall/v7/httpclient"
 11 | 	"github.com/gojek/heimdall/v7/hystrix"
 12 | 	"github.com/pkg/errors"
 13 | )
 14 | 
 15 | const (
 16 | 	baseURL = "http://localhost:9090"
 17 | )
 18 | 
 19 | func httpClientUsage() error {
 20 | 	timeout := 100 * time.Millisecond
 21 | 
 22 | 	httpClient := httpclient.NewClient(
 23 | 		httpclient.WithHTTPTimeout(timeout),
 24 | 		httpclient.WithRetryCount(2),
 25 | 		httpclient.WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(10*time.Millisecond, 50*time.Millisecond))),
 26 | 	)
 27 | 	headers := http.Header{}
 28 | 	headers.Set("Content-Type", "application/json")
 29 | 
 30 | 	response, err := httpClient.Get(baseURL, headers)
 31 | 	if err != nil {
 32 | 		return errors.Wrap(err, "failed to make a request to server")
 33 | 	}
 34 | 
 35 | 	defer response.Body.Close()
 36 | 
 37 | 	respBody, err := ioutil.ReadAll(response.Body)
 38 | 	if err != nil {
 39 | 		return errors.Wrap(err, "failed to read response body")
 40 | 	}
 41 | 
 42 | 	fmt.Printf("Response: %s", string(respBody))
 43 | 	return nil
 44 | }
 45 | 
 46 | func hystrixClientUsage() error {
 47 | 	timeout := 100 * time.Millisecond
 48 | 	hystrixClient := hystrix.NewClient(
 49 | 		hystrix.WithHTTPTimeout(timeout),
 50 | 		hystrix.WithCommandName("MyCommand"),
 51 | 		hystrix.WithHystrixTimeout(1100*time.Millisecond),
 52 | 		hystrix.WithMaxConcurrentRequests(100),
 53 | 		hystrix.WithErrorPercentThreshold(25),
 54 | 		hystrix.WithSleepWindow(10),
 55 | 		hystrix.WithRequestVolumeThreshold(10),
 56 | 		hystrix.WithStatsDCollector("localhost:8125", "myapp.hystrix"),
 57 | 	)
 58 | 	headers := http.Header{}
 59 | 	response, err := hystrixClient.Get(baseURL, headers)
 60 | 	if err != nil {
 61 | 		return errors.Wrap(err, "failed to make a request to server")
 62 | 	}
 63 | 
 64 | 	defer response.Body.Close()
 65 | 
 66 | 	respBody, err := ioutil.ReadAll(response.Body)
 67 | 	if err != nil {
 68 | 		return errors.Wrap(err, "failed to read response body")
 69 | 	}
 70 | 
 71 | 	fmt.Printf("Response: %s", string(respBody))
 72 | 	return nil
 73 | }
 74 | 
 75 | type myHTTPClient struct {
 76 | 	client http.Client
 77 | }
 78 | 
 79 | func (c *myHTTPClient) Do(request *http.Request) (*http.Response, error) {
 80 | 	request.SetBasicAuth("username", "passwd")
 81 | 	return c.client.Do(request)
 82 | }
 83 | 
 84 | func customHTTPClientUsage() error {
 85 | 	httpClient := httpclient.NewClient(
 86 | 		httpclient.WithHTTPTimeout(0*time.Millisecond),
 87 | 		httpclient.WithHTTPClient(&myHTTPClient{
 88 | 			// replace with custom HTTP client
 89 | 			client: http.Client{Timeout: 25 * time.Millisecond},
 90 | 		}),
 91 | 		httpclient.WithRetryCount(2),
 92 | 		httpclient.WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(10*time.Millisecond, 50*time.Millisecond))),
 93 | 	)
 94 | 
 95 | 	headers := http.Header{}
 96 | 	headers.Set("Content-Type", "application/json")
 97 | 
 98 | 	response, err := httpClient.Get(baseURL, headers)
 99 | 	if err != nil {
100 | 		return errors.Wrap(err, "failed to make a request to server")
101 | 	}
102 | 
103 | 	defer response.Body.Close()
104 | 
105 | 	respBody, err := ioutil.ReadAll(response.Body)
106 | 	if err != nil {
107 | 		return errors.Wrap(err, "failed to read response body")
108 | 	}
109 | 
110 | 	fmt.Printf("Response: %s", string(respBody))
111 | 	return nil
112 | }
113 | 
114 | func customHystrixClientUsage() error {
115 | 	timeout := 0 * time.Millisecond
116 | 
117 | 	hystrixClient := hystrix.NewClient(
118 | 		hystrix.WithHTTPTimeout(timeout),
119 | 		hystrix.WithCommandName("MyCommand"),
120 | 		hystrix.WithHystrixTimeout(1100*time.Millisecond),
121 | 		hystrix.WithMaxConcurrentRequests(100),
122 | 		hystrix.WithErrorPercentThreshold(25),
123 | 		hystrix.WithSleepWindow(10),
124 | 		hystrix.WithRequestVolumeThreshold(10),
125 | 		hystrix.WithHTTPClient(&myHTTPClient{
126 | 			// replace with custom HTTP client
127 | 			client: http.Client{Timeout: 25 * time.Millisecond},
128 | 		}),
129 | 	)
130 | 
131 | 	headers := http.Header{}
132 | 	response, err := hystrixClient.Get(baseURL, headers)
133 | 	if err != nil {
134 | 		return errors.Wrap(err, "failed to make a request to server")
135 | 	}
136 | 
137 | 	defer response.Body.Close()
138 | 
139 | 	respBody, err := ioutil.ReadAll(response.Body)
140 | 	if err != nil {
141 | 		return errors.Wrap(err, "failed to read response body")
142 | 	}
143 | 
144 | 	fmt.Printf("Response: %s", string(respBody))
145 | 	return nil
146 | }
147 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module github.com/gojek/heimdall/v7
 2 | 
 3 | go 1.14
 4 | 
 5 | require (
 6 | 	github.com/DataDog/datadog-go v3.7.1+incompatible // indirect
 7 | 	github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
 8 | 	github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c // indirect
 9 | 	github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf
10 | 	github.com/pkg/errors v0.9.1
11 | 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
12 | 	github.com/smartystreets/goconvey v1.6.4 // indirect
13 | 	github.com/stretchr/objx v0.3.0 // indirect
14 | 	github.com/stretchr/testify v1.3.0
15 | )
16 | 


--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
 1 | github.com/DataDog/datadog-go v3.7.1+incompatible h1:HmA9qHVrHIAqpSvoCYJ+c6qst0lgqEhNW6/KwfkHbS8=
 2 | github.com/DataDog/datadog-go v3.7.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
 3 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw=
 4 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
 5 | github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c h1:HIGF0r/56+7fuIZw2V4isE22MK6xpxWx7BbV8dJ290w=
 6 | github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf h1:5xRGbUdOmZKoDXkGx5evVLehuCMpuO1hl701bEQqXOM=
12 | github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf/go.mod h1:QzhUKaYKJmcbTnCYCAVQrroCOY7vOOI8cSQ4NbuhYf0=
13 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
14 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
15 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
16 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
17 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
21 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
22 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
23 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
24 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
25 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
26 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
28 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
29 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
30 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
31 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
32 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
33 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
34 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
35 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
36 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
37 | 


--------------------------------------------------------------------------------
/httpclient/client.go:
--------------------------------------------------------------------------------
  1 | package httpclient
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"io"
  6 | 	"io/ioutil"
  7 | 	"net/http"
  8 | 	"time"
  9 | 
 10 | 	"github.com/gojek/heimdall/v7"
 11 | 	"github.com/gojek/valkyrie"
 12 | 	"github.com/pkg/errors"
 13 | )
 14 | 
 15 | // Client is the http client implementation
 16 | type Client struct {
 17 | 	client heimdall.Doer
 18 | 
 19 | 	timeout    time.Duration
 20 | 	retryCount int
 21 | 	retrier    heimdall.Retriable
 22 | 	plugins    []heimdall.Plugin
 23 | }
 24 | 
 25 | const (
 26 | 	defaultRetryCount  = 0
 27 | 	defaultHTTPTimeout = 30 * time.Second
 28 | )
 29 | 
 30 | var _ heimdall.Client = (*Client)(nil)
 31 | 
 32 | // NewClient returns a new instance of http Client
 33 | func NewClient(opts ...Option) *Client {
 34 | 	client := Client{
 35 | 		timeout:    defaultHTTPTimeout,
 36 | 		retryCount: defaultRetryCount,
 37 | 		retrier:    heimdall.NewNoRetrier(),
 38 | 	}
 39 | 
 40 | 	for _, opt := range opts {
 41 | 		opt(&client)
 42 | 	}
 43 | 
 44 | 	if client.client == nil {
 45 | 		client.client = &http.Client{
 46 | 			Timeout: client.timeout,
 47 | 		}
 48 | 	}
 49 | 
 50 | 	return &client
 51 | }
 52 | 
 53 | // AddPlugin Adds plugin to client
 54 | func (c *Client) AddPlugin(p heimdall.Plugin) {
 55 | 	c.plugins = append(c.plugins, p)
 56 | }
 57 | 
 58 | // Get makes a HTTP GET request to provided URL
 59 | func (c *Client) Get(url string, headers http.Header) (*http.Response, error) {
 60 | 	var response *http.Response
 61 | 	request, err := http.NewRequest(http.MethodGet, url, nil)
 62 | 	if err != nil {
 63 | 		return response, errors.Wrap(err, "GET - request creation failed")
 64 | 	}
 65 | 
 66 | 	request.Header = headers
 67 | 
 68 | 	return c.Do(request)
 69 | }
 70 | 
 71 | // Post makes a HTTP POST request to provided URL and requestBody
 72 | func (c *Client) Post(url string, body io.Reader, headers http.Header) (*http.Response, error) {
 73 | 	var response *http.Response
 74 | 	request, err := http.NewRequest(http.MethodPost, url, body)
 75 | 	if err != nil {
 76 | 		return response, errors.Wrap(err, "POST - request creation failed")
 77 | 	}
 78 | 
 79 | 	request.Header = headers
 80 | 
 81 | 	return c.Do(request)
 82 | }
 83 | 
 84 | // Put makes a HTTP PUT request to provided URL and requestBody
 85 | func (c *Client) Put(url string, body io.Reader, headers http.Header) (*http.Response, error) {
 86 | 	var response *http.Response
 87 | 	request, err := http.NewRequest(http.MethodPut, url, body)
 88 | 	if err != nil {
 89 | 		return response, errors.Wrap(err, "PUT - request creation failed")
 90 | 	}
 91 | 
 92 | 	request.Header = headers
 93 | 
 94 | 	return c.Do(request)
 95 | }
 96 | 
 97 | // Patch makes a HTTP PATCH request to provided URL and requestBody
 98 | func (c *Client) Patch(url string, body io.Reader, headers http.Header) (*http.Response, error) {
 99 | 	var response *http.Response
100 | 	request, err := http.NewRequest(http.MethodPatch, url, body)
101 | 	if err != nil {
102 | 		return response, errors.Wrap(err, "PATCH - request creation failed")
103 | 	}
104 | 
105 | 	request.Header = headers
106 | 
107 | 	return c.Do(request)
108 | }
109 | 
110 | // Delete makes a HTTP DELETE request with provided URL
111 | func (c *Client) Delete(url string, headers http.Header) (*http.Response, error) {
112 | 	var response *http.Response
113 | 	request, err := http.NewRequest(http.MethodDelete, url, nil)
114 | 	if err != nil {
115 | 		return response, errors.Wrap(err, "DELETE - request creation failed")
116 | 	}
117 | 
118 | 	request.Header = headers
119 | 
120 | 	return c.Do(request)
121 | }
122 | 
123 | // Do makes an HTTP request with the native `http.Do` interface
124 | func (c *Client) Do(request *http.Request) (*http.Response, error) {
125 | 	var bodyReader *bytes.Reader
126 | 
127 | 	if request.Body != nil {
128 | 		reqData, err := ioutil.ReadAll(request.Body)
129 | 		if err != nil {
130 | 			return nil, err
131 | 		}
132 | 		bodyReader = bytes.NewReader(reqData)
133 | 		request.Body = ioutil.NopCloser(bodyReader) // prevents closing the body between retries
134 | 	}
135 | 
136 | 	multiErr := &valkyrie.MultiError{}
137 | 	var response *http.Response
138 | 
139 | 	for i := 0; i <= c.retryCount; i++ {
140 | 		if response != nil {
141 | 			response.Body.Close()
142 | 		}
143 | 
144 | 		c.reportRequestStart(request)
145 | 		var err error
146 | 		response, err = c.client.Do(request)
147 | 		if bodyReader != nil {
148 | 			// Reset the body reader after the request since at this point it's already read
149 | 			// Note that it's safe to ignore the error here since the 0,0 position is always valid
150 | 			_, _ = bodyReader.Seek(0, 0)
151 | 		}
152 | 
153 | 		if err != nil {
154 | 			multiErr.Push(err.Error())
155 | 			c.reportError(request, err)
156 | 			backoffTime := c.retrier.NextInterval(i)
157 | 			time.Sleep(backoffTime)
158 | 			continue
159 | 		}
160 | 		c.reportRequestEnd(request, response)
161 | 
162 | 		if response.StatusCode >= http.StatusInternalServerError {
163 | 			backoffTime := c.retrier.NextInterval(i)
164 | 			time.Sleep(backoffTime)
165 | 			continue
166 | 		}
167 | 
168 | 		multiErr = &valkyrie.MultiError{} // Clear errors if any iteration succeeds
169 | 		break
170 | 	}
171 | 
172 | 	return response, multiErr.HasError()
173 | }
174 | 
175 | func (c *Client) reportRequestStart(request *http.Request) {
176 | 	for _, plugin := range c.plugins {
177 | 		plugin.OnRequestStart(request)
178 | 	}
179 | }
180 | 
181 | func (c *Client) reportError(request *http.Request, err error) {
182 | 	for _, plugin := range c.plugins {
183 | 		plugin.OnError(request, err)
184 | 	}
185 | }
186 | 
187 | func (c *Client) reportRequestEnd(request *http.Request, response *http.Response) {
188 | 	for _, plugin := range c.plugins {
189 | 		plugin.OnRequestEnd(request, response)
190 | 	}
191 | }
192 | 


--------------------------------------------------------------------------------
/httpclient/client_test.go:
--------------------------------------------------------------------------------
  1 | package httpclient
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"io/ioutil"
  6 | 	"net/http"
  7 | 	"net/http/httptest"
  8 | 	"strings"
  9 | 	"testing"
 10 | 	"time"
 11 | 
 12 | 	"github.com/gojek/heimdall/v7"
 13 | 	"github.com/stretchr/testify/assert"
 14 | 	"github.com/stretchr/testify/mock"
 15 | 	"github.com/stretchr/testify/require"
 16 | )
 17 | 
 18 | func TestHTTPClientDoSuccess(t *testing.T) {
 19 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
 20 | 
 21 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
 22 | 		assert.Equal(t, http.MethodGet, r.Method)
 23 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
 24 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
 25 | 
 26 | 		w.WriteHeader(http.StatusOK)
 27 | 		w.Write([]byte(`{ "response": "ok" }`))
 28 | 	}
 29 | 
 30 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
 31 | 	defer server.Close()
 32 | 
 33 | 	req, err := http.NewRequest(http.MethodGet, server.URL, nil)
 34 | 	require.NoError(t, err)
 35 | 	req.Header.Set("Content-Type", "application/json")
 36 | 	req.Header.Set("Accept-Language", "en")
 37 | 	response, err := client.Do(req)
 38 | 	require.NoError(t, err, "should not have failed to make a GET request")
 39 | 
 40 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
 41 | 
 42 | 	body, err := ioutil.ReadAll(response.Body)
 43 | 	require.NoError(t, err)
 44 | 	assert.Equal(t, "{ \"response\": \"ok\" }", string(body))
 45 | }
 46 | 
 47 | func TestHTTPClientGetSuccess(t *testing.T) {
 48 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
 49 | 
 50 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
 51 | 		assert.Equal(t, http.MethodGet, r.Method)
 52 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
 53 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
 54 | 
 55 | 		w.WriteHeader(http.StatusOK)
 56 | 		w.Write([]byte(`{ "response": "ok" }`))
 57 | 	}
 58 | 
 59 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
 60 | 	defer server.Close()
 61 | 
 62 | 	headers := http.Header{}
 63 | 	headers.Set("Content-Type", "application/json")
 64 | 	headers.Set("Accept-Language", "en")
 65 | 
 66 | 	response, err := client.Get(server.URL, headers)
 67 | 	require.NoError(t, err, "should not have failed to make a GET request")
 68 | 
 69 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
 70 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
 71 | }
 72 | 
 73 | func TestHTTPClientPostSuccess(t *testing.T) {
 74 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
 75 | 
 76 | 	requestBodyString := `{ "name": "heimdall" }`
 77 | 
 78 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
 79 | 		assert.Equal(t, http.MethodPost, r.Method)
 80 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
 81 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
 82 | 
 83 | 		rBody, err := ioutil.ReadAll(r.Body)
 84 | 		require.NoError(t, err, "should not have failed to extract request body")
 85 | 
 86 | 		assert.Equal(t, requestBodyString, string(rBody))
 87 | 
 88 | 		w.WriteHeader(http.StatusOK)
 89 | 		w.Write([]byte(`{ "response": "ok" }`))
 90 | 	}
 91 | 
 92 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
 93 | 	defer server.Close()
 94 | 
 95 | 	requestBody := bytes.NewReader([]byte(requestBodyString))
 96 | 
 97 | 	headers := http.Header{}
 98 | 	headers.Set("Content-Type", "application/json")
 99 | 	headers.Set("Accept-Language", "en")
100 | 
101 | 	response, err := client.Post(server.URL, requestBody, headers)
102 | 	require.NoError(t, err, "should not have failed to make a POST request")
103 | 
104 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
105 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
106 | }
107 | 
108 | func TestHTTPClientDeleteSuccess(t *testing.T) {
109 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
110 | 
111 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
112 | 		assert.Equal(t, http.MethodDelete, r.Method)
113 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
114 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
115 | 
116 | 		w.WriteHeader(http.StatusOK)
117 | 		w.Write([]byte(`{ "response": "ok" }`))
118 | 	}
119 | 
120 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
121 | 	defer server.Close()
122 | 
123 | 	headers := http.Header{}
124 | 	headers.Set("Content-Type", "application/json")
125 | 	headers.Set("Accept-Language", "en")
126 | 
127 | 	response, err := client.Delete(server.URL, headers)
128 | 	require.NoError(t, err, "should not have failed to make a DELETE request")
129 | 
130 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
131 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
132 | }
133 | 
134 | func TestHTTPClientPutSuccess(t *testing.T) {
135 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
136 | 
137 | 	requestBodyString := `{ "name": "heimdall" }`
138 | 
139 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
140 | 		assert.Equal(t, http.MethodPut, r.Method)
141 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
142 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
143 | 
144 | 		rBody, err := ioutil.ReadAll(r.Body)
145 | 		require.NoError(t, err, "should not have failed to extract request body")
146 | 
147 | 		assert.Equal(t, requestBodyString, string(rBody))
148 | 
149 | 		w.WriteHeader(http.StatusOK)
150 | 		w.Write([]byte(`{ "response": "ok" }`))
151 | 	}
152 | 
153 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
154 | 	defer server.Close()
155 | 
156 | 	requestBody := bytes.NewReader([]byte(requestBodyString))
157 | 
158 | 	headers := http.Header{}
159 | 	headers.Set("Content-Type", "application/json")
160 | 	headers.Set("Accept-Language", "en")
161 | 
162 | 	response, err := client.Put(server.URL, requestBody, headers)
163 | 	require.NoError(t, err, "should not have failed to make a PUT request")
164 | 
165 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
166 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
167 | }
168 | 
169 | func TestHTTPClientPatchSuccess(t *testing.T) {
170 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
171 | 
172 | 	requestBodyString := `{ "name": "heimdall" }`
173 | 
174 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
175 | 		assert.Equal(t, http.MethodPatch, r.Method)
176 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
177 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
178 | 
179 | 		rBody, err := ioutil.ReadAll(r.Body)
180 | 		require.NoError(t, err, "should not have failed to extract request body")
181 | 
182 | 		assert.Equal(t, requestBodyString, string(rBody))
183 | 
184 | 		w.WriteHeader(http.StatusOK)
185 | 		w.Write([]byte(`{ "response": "ok" }`))
186 | 	}
187 | 
188 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
189 | 	defer server.Close()
190 | 
191 | 	requestBody := bytes.NewReader([]byte(requestBodyString))
192 | 
193 | 	headers := http.Header{}
194 | 	headers.Set("Content-Type", "application/json")
195 | 	headers.Set("Accept-Language", "en")
196 | 
197 | 	response, err := client.Patch(server.URL, requestBody, headers)
198 | 	require.NoError(t, err, "should not have failed to make a PATCH request")
199 | 
200 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
201 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
202 | }
203 | 
204 | func TestHTTPClientGetRetriesOnFailure(t *testing.T) {
205 | 	count := 0
206 | 	noOfRetries := 3
207 | 	noOfCalls := noOfRetries + 1
208 | 	backoffInterval := 1 * time.Millisecond
209 | 	maximumJitterInterval := 1 * time.Millisecond
210 | 
211 | 	client := NewClient(
212 | 		WithHTTPTimeout(10*time.Millisecond),
213 | 		WithRetryCount(noOfRetries),
214 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
215 | 	)
216 | 
217 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
218 | 		w.WriteHeader(http.StatusInternalServerError)
219 | 		w.Write([]byte(`{ "response": "something went wrong" }`))
220 | 		count++
221 | 	}
222 | 
223 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
224 | 	defer server.Close()
225 | 
226 | 	response, err := client.Get(server.URL, http.Header{})
227 | 	require.NoError(t, err, "should have failed to make GET request")
228 | 
229 | 	require.Equal(t, http.StatusInternalServerError, response.StatusCode)
230 | 	require.Equal(t, "{ \"response\": \"something went wrong\" }", respBody(t, response))
231 | 
232 | 	assert.Equal(t, noOfCalls, count)
233 | }
234 | 
235 | func BenchmarkHTTPClientGetRetriesOnFailure(b *testing.B) {
236 | 	noOfRetries := 3
237 | 	backoffInterval := 1 * time.Millisecond
238 | 	maximumJitterInterval := 1 * time.Millisecond
239 | 
240 | 	client := NewClient(
241 | 		WithHTTPTimeout(10*time.Millisecond),
242 | 		WithRetryCount(noOfRetries),
243 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
244 | 	)
245 | 
246 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
247 | 		w.WriteHeader(http.StatusInternalServerError)
248 | 		w.Write([]byte(`{ "response": "something went wrong" }`))
249 | 	}
250 | 
251 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
252 | 	defer server.Close()
253 | 
254 | 	for i := 0; i < b.N; i++ {
255 | 		_, _ = client.Get(server.URL, http.Header{})
256 | 	}
257 | }
258 | 
259 | func TestHTTPClientPostRetriesOnFailure(t *testing.T) {
260 | 	count := 0
261 | 	noOfRetries := 3
262 | 	noOfCalls := noOfRetries + 1
263 | 	backoffInterval := 1 * time.Millisecond
264 | 	maximumJitterInterval := 1 * time.Millisecond
265 | 
266 | 	client := NewClient(
267 | 		WithHTTPTimeout(10*time.Millisecond),
268 | 		WithRetryCount(noOfRetries),
269 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
270 | 	)
271 | 
272 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
273 | 		w.WriteHeader(http.StatusInternalServerError)
274 | 		w.Write([]byte(`{ "response": "something went wrong" }`))
275 | 		count++
276 | 	}
277 | 
278 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
279 | 	defer server.Close()
280 | 
281 | 	response, err := client.Post(server.URL, strings.NewReader("a=1"), http.Header{})
282 | 	require.NoError(t, err, "should have failed to make GET request")
283 | 
284 | 	require.Equal(t, http.StatusInternalServerError, response.StatusCode)
285 | 	require.Equal(t, "{ \"response\": \"something went wrong\" }", respBody(t, response))
286 | 
287 | 	assert.Equal(t, noOfCalls, count)
288 | }
289 | 
290 | func BenchmarkHTTPClientPostRetriesOnFailure(b *testing.B) {
291 | 	noOfRetries := 3
292 | 	backoffInterval := 1 * time.Millisecond
293 | 	maximumJitterInterval := 1 * time.Millisecond
294 | 
295 | 	client := NewClient(
296 | 		WithHTTPTimeout(10*time.Millisecond),
297 | 		WithRetryCount(noOfRetries),
298 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
299 | 	)
300 | 
301 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
302 | 		w.WriteHeader(http.StatusInternalServerError)
303 | 		w.Write([]byte(`{ "response": "something went wrong" }`))
304 | 	}
305 | 
306 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
307 | 	defer server.Close()
308 | 
309 | 	for i := 0; i < b.N; i++ {
310 | 		_, _ = client.Post(server.URL, strings.NewReader("a=1"), http.Header{})
311 | 	}
312 | }
313 | 
314 | func TestHTTPClientGetReturnsNoErrorsIfRetriesFailWith5xx(t *testing.T) {
315 | 	count := 0
316 | 	noOfRetries := 2
317 | 	backoffInterval := 1 * time.Millisecond
318 | 	maximumJitterInterval := 1 * time.Millisecond
319 | 
320 | 	client := NewClient(
321 | 		WithHTTPTimeout(10*time.Millisecond),
322 | 		WithRetryCount(noOfRetries),
323 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
324 | 	)
325 | 
326 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
327 | 		w.WriteHeader(http.StatusInternalServerError)
328 | 		w.Write([]byte(`{ "response": "something went wrong" }`))
329 | 		count++
330 | 	}
331 | 
332 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
333 | 	defer server.Close()
334 | 
335 | 	response, err := client.Get(server.URL, http.Header{})
336 | 	require.NoError(t, err)
337 | 
338 | 	require.Equal(t, noOfRetries+1, count)
339 | 	require.Equal(t, http.StatusInternalServerError, response.StatusCode)
340 | 	require.Equal(t, "{ \"response\": \"something went wrong\" }", respBody(t, response))
341 | }
342 | 
343 | func TestHTTPClientGetReturnsNoErrorsIfRetrySucceeds(t *testing.T) {
344 | 	count := 0
345 | 	countWhenCallSucceeds := 2
346 | 	backoffInterval := 1 * time.Millisecond
347 | 	maximumJitterInterval := 1 * time.Millisecond
348 | 
349 | 	client := NewClient(
350 | 		WithHTTPTimeout(10*time.Millisecond),
351 | 		WithRetryCount(3),
352 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
353 | 	)
354 | 
355 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
356 | 		if count == countWhenCallSucceeds {
357 | 			w.Write([]byte(`{ "response": "success" }`))
358 | 		} else {
359 | 			w.WriteHeader(http.StatusInternalServerError)
360 | 			w.Write([]byte(`{ "response": "something went wrong" }`))
361 | 		}
362 | 		count++
363 | 	}
364 | 
365 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
366 | 	defer server.Close()
367 | 
368 | 	response, err := client.Get(server.URL, http.Header{})
369 | 	require.NoError(t, err, "should not have failed to make GET request")
370 | 
371 | 	require.Equal(t, countWhenCallSucceeds+1, count)
372 | 	require.Equal(t, http.StatusOK, response.StatusCode)
373 | 	require.Equal(t, "{ \"response\": \"success\" }", respBody(t, response))
374 | }
375 | 
376 | func TestHTTPClientGetReturnsErrorOnClientCallFailure(t *testing.T) {
377 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
378 | 
379 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
380 | 		w.WriteHeader(http.StatusOK)
381 | 	}
382 | 
383 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
384 | 	server.URL = "" // Invalid URL to simulate client.Do error
385 | 	defer server.Close()
386 | 
387 | 	response, err := client.Get(server.URL, http.Header{})
388 | 	require.Error(t, err, "should have failed to make GET request")
389 | 
390 | 	require.Nil(t, response)
391 | 
392 | 	assert.Contains(t, err.Error(), "unsupported protocol scheme")
393 | }
394 | 
395 | func TestHTTPClientGetReturnsNoErrorOn5xxFailure(t *testing.T) {
396 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
397 | 
398 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
399 | 		w.WriteHeader(http.StatusInternalServerError)
400 | 		w.Write([]byte(`{ "response": "something went wrong" }`))
401 | 	}
402 | 
403 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
404 | 	defer server.Close()
405 | 
406 | 	response, err := client.Get(server.URL, http.Header{})
407 | 	require.NoError(t, err)
408 | 	require.Equal(t, http.StatusInternalServerError, response.StatusCode)
409 | 
410 | }
411 | 
412 | func TestHTTPClientGetReturnsErrorOnFailure(t *testing.T) {
413 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
414 | 
415 | 	response, err := client.Get("url_doenst_exist", http.Header{})
416 | 	assert.Contains(t, err.Error(), "unsupported protocol scheme")
417 | 	assert.Nil(t, response)
418 | }
419 | 
420 | func TestPluginMethodsCalled(t *testing.T) {
421 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
422 | 	mockPlugin := &MockPlugin{}
423 | 	client.AddPlugin(mockPlugin)
424 | 
425 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
426 | 		w.WriteHeader(http.StatusOK)
427 | 		w.Write([]byte(`{ "response": "something went wrong" }`))
428 | 	}
429 | 
430 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
431 | 	defer server.Close()
432 | 
433 | 	mockPlugin.On("OnRequestStart", mock.Anything)
434 | 	mockPlugin.On("OnRequestEnd", mock.Anything, mock.Anything)
435 | 
436 | 	_, err := client.Get(server.URL, http.Header{})
437 | 
438 | 	require.NoError(t, err)
439 | 	mockPlugin.AssertNumberOfCalls(t, "OnRequestStart", 1)
440 | 	pluginRequest, ok := mockPlugin.Calls[0].Arguments[0].(*http.Request)
441 | 	require.True(t, ok)
442 | 	assert.Equal(t, http.MethodGet, pluginRequest.Method)
443 | 	assert.Equal(t, server.URL, pluginRequest.URL.String())
444 | 
445 | 	mockPlugin.AssertNumberOfCalls(t, "OnRequestEnd", 1)
446 | 	pluginResponse, ok := mockPlugin.Calls[1].Arguments[1].(*http.Response)
447 | 	require.True(t, ok)
448 | 	assert.Equal(t, http.StatusOK, pluginResponse.StatusCode)
449 | }
450 | 
451 | func TestPluginErrorMethodCalled(t *testing.T) {
452 | 	client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
453 | 	mockPlugin := &MockPlugin{}
454 | 	client.AddPlugin(mockPlugin)
455 | 
456 | 	mockPlugin.On("OnRequestStart", mock.Anything)
457 | 	mockPlugin.On("OnError", mock.Anything, mock.Anything)
458 | 
459 | 	serverURL := "does_not_exist"
460 | 	_, err := client.Get(serverURL, http.Header{})
461 | 
462 | 	mockPlugin.AssertNumberOfCalls(t, "OnRequestStart", 1)
463 | 	pluginRequest, ok := mockPlugin.Calls[0].Arguments[0].(*http.Request)
464 | 	require.True(t, ok)
465 | 	assert.Equal(t, http.MethodGet, pluginRequest.Method)
466 | 	assert.Equal(t, serverURL, pluginRequest.URL.String())
467 | 
468 | 	mockPlugin.AssertNumberOfCalls(t, "OnError", 1)
469 | 	err, ok = mockPlugin.Calls[1].Arguments[1].(error)
470 | 	require.True(t, ok)
471 | 	assert.Contains(t, err.Error(), "unsupported protocol scheme")
472 | }
473 | 
474 | type myHTTPClient struct {
475 | 	client http.Client
476 | }
477 | 
478 | func (c *myHTTPClient) Do(request *http.Request) (*http.Response, error) {
479 | 	request.Header.Set("foo", "bar")
480 | 	return c.client.Do(request)
481 | }
482 | 
483 | func TestCustomHTTPClientHeaderSuccess(t *testing.T) {
484 | 	client := NewClient(
485 | 		WithHTTPTimeout(10*time.Millisecond),
486 | 		WithHTTPClient(&myHTTPClient{
487 | 			client: http.Client{Timeout: 25 * time.Millisecond}}),
488 | 	)
489 | 
490 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
491 | 		assert.Equal(t, "bar", r.Header.Get("foo"))
492 | 		assert.NotEqual(t, "baz", r.Header.Get("foo"))
493 | 		w.WriteHeader(http.StatusOK)
494 | 		w.Write([]byte(`{ "response": "ok" }`))
495 | 	}
496 | 
497 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
498 | 	defer server.Close()
499 | 
500 | 	req, err := http.NewRequest(http.MethodGet, server.URL, nil)
501 | 	require.NoError(t, err)
502 | 	response, err := client.Do(req)
503 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
504 | 
505 | 	body, err := ioutil.ReadAll(response.Body)
506 | 	require.NoError(t, err)
507 | 	assert.Equal(t, "{ \"response\": \"ok\" }", string(body))
508 | }
509 | 
510 | func respBody(t *testing.T, response *http.Response) string {
511 | 	if response.Body != nil {
512 | 		defer response.Body.Close()
513 | 	}
514 | 
515 | 	respBody, err := ioutil.ReadAll(response.Body)
516 | 	require.NoError(t, err, "should not have failed to read response body")
517 | 
518 | 	return string(respBody)
519 | }
520 | 


--------------------------------------------------------------------------------
/httpclient/options.go:
--------------------------------------------------------------------------------
 1 | package httpclient
 2 | 
 3 | import (
 4 | 	"time"
 5 | 
 6 | 	"github.com/gojek/heimdall/v7"
 7 | )
 8 | 
 9 | // Option represents the client options
10 | type Option func(*Client)
11 | 
12 | // WithHTTPTimeout sets hystrix timeout
13 | func WithHTTPTimeout(timeout time.Duration) Option {
14 | 	return func(c *Client) {
15 | 		c.timeout = timeout
16 | 	}
17 | }
18 | 
19 | // WithRetryCount sets the retry count for the hystrixHTTPClient
20 | func WithRetryCount(retryCount int) Option {
21 | 	return func(c *Client) {
22 | 		c.retryCount = retryCount
23 | 	}
24 | }
25 | 
26 | // WithRetrier sets the strategy for retrying
27 | func WithRetrier(retrier heimdall.Retriable) Option {
28 | 	return func(c *Client) {
29 | 		c.retrier = retrier
30 | 	}
31 | }
32 | 
33 | // WithHTTPClient sets a custom http client
34 | func WithHTTPClient(client heimdall.Doer) Option {
35 | 	return func(c *Client) {
36 | 		c.client = client
37 | 	}
38 | }
39 | 


--------------------------------------------------------------------------------
/httpclient/options_test.go:
--------------------------------------------------------------------------------
  1 | package httpclient
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"net/http"
  6 | 	"testing"
  7 | 	"time"
  8 | 
  9 | 	"github.com/gojek/heimdall/v7"
 10 | 	"github.com/stretchr/testify/assert"
 11 | )
 12 | 
 13 | func TestOptionsAreSet(t *testing.T) {
 14 | 	backoffInterval := 1 * time.Millisecond
 15 | 	maximumJitterInterval := 1 * time.Millisecond
 16 | 	noOfRetries := 3
 17 | 	httpTimeout := 10 * time.Second
 18 | 
 19 | 	client := &myHTTPClient{client: http.Client{Timeout: 25 * time.Millisecond}}
 20 | 	retrier := heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))
 21 | 
 22 | 	c := NewClient(
 23 | 		WithHTTPClient(client),
 24 | 		WithHTTPTimeout(httpTimeout),
 25 | 		WithRetrier(retrier),
 26 | 		WithRetryCount(noOfRetries),
 27 | 	)
 28 | 
 29 | 	assert.Equal(t, client, c.client)
 30 | 	assert.Equal(t, httpTimeout, c.timeout)
 31 | 	assert.Equal(t, retrier, c.retrier)
 32 | 	assert.Equal(t, noOfRetries, c.retryCount)
 33 | }
 34 | 
 35 | func TestOptionsHaveDefaults(t *testing.T) {
 36 | 	retrier := heimdall.NewNoRetrier()
 37 | 	httpTimeout := 30 * time.Second
 38 | 	http.DefaultClient.Timeout = httpTimeout
 39 | 	noOfRetries := 0
 40 | 
 41 | 	c := NewClient()
 42 | 
 43 | 	assert.Equal(t, http.DefaultClient, c.client)
 44 | 	assert.Equal(t, httpTimeout, c.timeout)
 45 | 	assert.Equal(t, retrier, c.retrier)
 46 | 	assert.Equal(t, noOfRetries, c.retryCount)
 47 | }
 48 | 
 49 | func ExampleWithHTTPTimeout() {
 50 | 	c := NewClient(WithHTTPTimeout(5 * time.Second))
 51 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
 52 | 	if err != nil {
 53 | 		panic(err)
 54 | 	}
 55 | 	res, err := c.Do(req)
 56 | 	if err != nil {
 57 | 		panic(err)
 58 | 	}
 59 | 	fmt.Println("Response status : ", res.StatusCode)
 60 | 	// Output: Response status :  200
 61 | }
 62 | 
 63 | func ExampleWithHTTPTimeout_expired() {
 64 | 	c := NewClient(WithHTTPTimeout(1 * time.Millisecond))
 65 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
 66 | 	if err != nil {
 67 | 		panic(err)
 68 | 	}
 69 | 	res, err := c.Do(req)
 70 | 	if err != nil {
 71 | 		fmt.Println("error:", err)
 72 | 		return
 73 | 	}
 74 | 	fmt.Println("Response status : ", res.StatusCode)
 75 | }
 76 | 
 77 | func ExampleWithRetryCount() {
 78 | 	c := NewClient(WithHTTPTimeout(1*time.Millisecond), WithRetryCount(3))
 79 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
 80 | 	if err != nil {
 81 | 		panic(err)
 82 | 	}
 83 | 	res, err := c.Do(req)
 84 | 	if err != nil {
 85 | 		fmt.Println("error:", err)
 86 | 		return
 87 | 	}
 88 | 	fmt.Println("Response status : ", res.StatusCode)
 89 | }
 90 | 
 91 | type mockClient struct{}
 92 | 
 93 | func (m *mockClient) Do(r *http.Request) (*http.Response, error) {
 94 | 	fmt.Println("mock client called")
 95 | 	return &http.Response{}, nil
 96 | }
 97 | 
 98 | func ExampleWithHTTPClient() {
 99 | 	m := &mockClient{}
100 | 	c := NewClient(WithHTTPClient(m))
101 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
102 | 	if err != nil {
103 | 		panic(err)
104 | 	}
105 | 	_, _ = c.Do(req)
106 | 	// Output: mock client called
107 | }
108 | 
109 | type mockRetrier struct{}
110 | 
111 | func (m *mockRetrier) NextInterval(attempt int) time.Duration {
112 | 	fmt.Println("retry attempt", attempt)
113 | 	return time.Millisecond
114 | }
115 | 
116 | func ExampleWithRetrier() {
117 | 	c := NewClient(WithHTTPTimeout(1*time.Millisecond), WithRetryCount(3), WithRetrier(&mockRetrier{}))
118 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
119 | 	if err != nil {
120 | 		panic(err)
121 | 	}
122 | 	res, err := c.Do(req)
123 | 	if err != nil {
124 | 		fmt.Println("error")
125 | 		return
126 | 	}
127 | 	fmt.Println("Response status : ", res.StatusCode)
128 | 	// Output: retry attempt 0
129 | 	// retry attempt 1
130 | 	// retry attempt 2
131 | 	// retry attempt 3
132 | 	// error
133 | }
134 | 


--------------------------------------------------------------------------------
/httpclient/plugin_mock.go:
--------------------------------------------------------------------------------
 1 | package httpclient
 2 | 
 3 | import (
 4 | 	"net/http"
 5 | 
 6 | 	"github.com/stretchr/testify/mock"
 7 | )
 8 | 
 9 | // MockPlugin provides a mock plugin for heimdall
10 | type MockPlugin struct {
11 | 	mock.Mock
12 | }
13 | 
14 | // OnRequestStart is called when the request starts
15 | func (m *MockPlugin) OnRequestStart(req *http.Request) {
16 | 	m.Called(req)
17 | }
18 | 
19 | // OnRequestEnd is called when the request ends
20 | func (m *MockPlugin) OnRequestEnd(req *http.Request, res *http.Response) {
21 | 	m.Called(req, res)
22 | }
23 | 
24 | // OnError is called when the request errors out
25 | func (m *MockPlugin) OnError(req *http.Request, err error) {
26 | 	m.Called(req, err)
27 | }
28 | 


--------------------------------------------------------------------------------
/hystrix/hystrix_client.go:
--------------------------------------------------------------------------------
  1 | package hystrix
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"io"
  6 | 	"io/ioutil"
  7 | 	"net/http"
  8 | 	"time"
  9 | 
 10 | 	"github.com/afex/hystrix-go/hystrix"
 11 | 	metricCollector "github.com/afex/hystrix-go/hystrix/metric_collector"
 12 | 	"github.com/afex/hystrix-go/plugins"
 13 | 	"github.com/gojek/heimdall/v7"
 14 | 	"github.com/gojek/heimdall/v7/httpclient"
 15 | 	"github.com/pkg/errors"
 16 | )
 17 | 
 18 | type fallbackFunc func(error) error
 19 | 
 20 | // Client is the hystrix client implementation
 21 | type Client struct {
 22 | 	client *httpclient.Client
 23 | 
 24 | 	timeout                time.Duration
 25 | 	hystrixTimeout         time.Duration
 26 | 	hystrixCommandName     string
 27 | 	maxConcurrentRequests  int
 28 | 	requestVolumeThreshold int
 29 | 	sleepWindow            int
 30 | 	errorPercentThreshold  int
 31 | 	retryCount             int
 32 | 	retrier                heimdall.Retriable
 33 | 	fallbackFunc           func(err error) error
 34 | 	statsD                 *plugins.StatsdCollectorConfig
 35 | }
 36 | 
 37 | const (
 38 | 	defaultHystrixRetryCount      = 0
 39 | 	defaultHTTPTimeout            = 30 * time.Second
 40 | 	defaultHystrixTimeout         = 30 * time.Second
 41 | 	defaultMaxConcurrentRequests  = 100
 42 | 	defaultErrorPercentThreshold  = 25
 43 | 	defaultSleepWindow            = 10
 44 | 	defaultRequestVolumeThreshold = 10
 45 | 
 46 | 	maxUint = ^uint(0)
 47 | 	maxInt  = int(maxUint >> 1)
 48 | )
 49 | 
 50 | var _ heimdall.Client = (*Client)(nil)
 51 | var err5xx = errors.New("server returned 5xx status code")
 52 | 
 53 | // NewClient returns a new instance of hystrix Client
 54 | func NewClient(opts ...Option) *Client {
 55 | 	client := Client{
 56 | 		client:                 httpclient.NewClient(),
 57 | 		timeout:                defaultHTTPTimeout,
 58 | 		hystrixTimeout:         defaultHystrixTimeout,
 59 | 		maxConcurrentRequests:  defaultMaxConcurrentRequests,
 60 | 		errorPercentThreshold:  defaultErrorPercentThreshold,
 61 | 		sleepWindow:            defaultSleepWindow,
 62 | 		requestVolumeThreshold: defaultRequestVolumeThreshold,
 63 | 		retryCount:             defaultHystrixRetryCount,
 64 | 		retrier:                heimdall.NewNoRetrier(),
 65 | 	}
 66 | 
 67 | 	for _, opt := range opts {
 68 | 		opt(&client)
 69 | 	}
 70 | 
 71 | 	if client.statsD != nil {
 72 | 		c, err := plugins.InitializeStatsdCollector(client.statsD)
 73 | 		if err != nil {
 74 | 			panic(err)
 75 | 		}
 76 | 
 77 | 		metricCollector.Registry.Register(c.NewStatsdCollector)
 78 | 	}
 79 | 
 80 | 	hystrix.ConfigureCommand(client.hystrixCommandName, hystrix.CommandConfig{
 81 | 		Timeout:                durationToInt(client.hystrixTimeout, time.Millisecond),
 82 | 		MaxConcurrentRequests:  client.maxConcurrentRequests,
 83 | 		RequestVolumeThreshold: client.requestVolumeThreshold,
 84 | 		SleepWindow:            client.sleepWindow,
 85 | 		ErrorPercentThreshold:  client.errorPercentThreshold,
 86 | 	})
 87 | 
 88 | 	return &client
 89 | }
 90 | 
 91 | func durationToInt(duration, unit time.Duration) int {
 92 | 	durationAsNumber := duration / unit
 93 | 
 94 | 	if int64(durationAsNumber) > int64(maxInt) {
 95 | 		// Returning max possible value seems like best possible solution here
 96 | 		// the alternative is to panic as there is no way of returning an error
 97 | 		// without changing the NewClient API
 98 | 		return maxInt
 99 | 	}
100 | 	return int(durationAsNumber)
101 | }
102 | 
103 | // Get makes a HTTP GET request to provided URL
104 | func (hhc *Client) Get(url string, headers http.Header) (*http.Response, error) {
105 | 	var response *http.Response
106 | 	request, err := http.NewRequest(http.MethodGet, url, nil)
107 | 	if err != nil {
108 | 		return response, errors.Wrap(err, "GET - request creation failed")
109 | 	}
110 | 
111 | 	request.Header = headers
112 | 
113 | 	return hhc.Do(request)
114 | }
115 | 
116 | // Post makes a HTTP POST request to provided URL and requestBody
117 | func (hhc *Client) Post(url string, body io.Reader, headers http.Header) (*http.Response, error) {
118 | 	var response *http.Response
119 | 	request, err := http.NewRequest(http.MethodPost, url, body)
120 | 	if err != nil {
121 | 		return response, errors.Wrap(err, "POST - request creation failed")
122 | 	}
123 | 
124 | 	request.Header = headers
125 | 
126 | 	return hhc.Do(request)
127 | }
128 | 
129 | // Put makes a HTTP PUT request to provided URL and requestBody
130 | func (hhc *Client) Put(url string, body io.Reader, headers http.Header) (*http.Response, error) {
131 | 	var response *http.Response
132 | 	request, err := http.NewRequest(http.MethodPut, url, body)
133 | 	if err != nil {
134 | 		return response, errors.Wrap(err, "PUT - request creation failed")
135 | 	}
136 | 
137 | 	request.Header = headers
138 | 
139 | 	return hhc.Do(request)
140 | }
141 | 
142 | // Patch makes a HTTP PATCH request to provided URL and requestBody
143 | func (hhc *Client) Patch(url string, body io.Reader, headers http.Header) (*http.Response, error) {
144 | 	var response *http.Response
145 | 	request, err := http.NewRequest(http.MethodPatch, url, body)
146 | 	if err != nil {
147 | 		return response, errors.Wrap(err, "PATCH - request creation failed")
148 | 	}
149 | 
150 | 	request.Header = headers
151 | 
152 | 	return hhc.Do(request)
153 | }
154 | 
155 | // Delete makes a HTTP DELETE request with provided URL
156 | func (hhc *Client) Delete(url string, headers http.Header) (*http.Response, error) {
157 | 	var response *http.Response
158 | 	request, err := http.NewRequest(http.MethodDelete, url, nil)
159 | 	if err != nil {
160 | 		return response, errors.Wrap(err, "DELETE - request creation failed")
161 | 	}
162 | 
163 | 	request.Header = headers
164 | 
165 | 	return hhc.Do(request)
166 | }
167 | 
168 | // Do makes an HTTP request with the native `http.Do` interface
169 | func (hhc *Client) Do(request *http.Request) (*http.Response, error) {
170 | 	var response *http.Response
171 | 	var err error
172 | 
173 | 	var bodyReader *bytes.Reader
174 | 
175 | 	if request.Body != nil {
176 | 		reqData, err := ioutil.ReadAll(request.Body)
177 | 		if err != nil {
178 | 			return nil, err
179 | 		}
180 | 		bodyReader = bytes.NewReader(reqData)
181 | 		request.Body = ioutil.NopCloser(bodyReader) // prevents closing the body between retries
182 | 	}
183 | 
184 | 	for i := 0; i <= hhc.retryCount; i++ {
185 | 		if response != nil {
186 | 			response.Body.Close()
187 | 		}
188 | 
189 | 		err = hystrix.Do(hhc.hystrixCommandName, func() error {
190 | 			response, err = hhc.client.Do(request)
191 | 			if bodyReader != nil {
192 | 				// Reset the body reader after the request since at this point it's already read
193 | 				// Note that it's safe to ignore the error here since the 0,0 position is always valid
194 | 				_, _ = bodyReader.Seek(0, 0)
195 | 			}
196 | 
197 | 			if err != nil {
198 | 				return err
199 | 			}
200 | 
201 | 			if response.StatusCode >= http.StatusInternalServerError {
202 | 				return err5xx
203 | 			}
204 | 			return nil
205 | 		}, hhc.fallbackFunc)
206 | 
207 | 		if err != nil {
208 | 			backoffTime := hhc.retrier.NextInterval(i)
209 | 			time.Sleep(backoffTime)
210 | 			continue
211 | 		}
212 | 
213 | 		break
214 | 	}
215 | 
216 | 	if err == err5xx {
217 | 		return response, nil
218 | 	}
219 | 
220 | 	return response, err
221 | }
222 | 
223 | // AddPlugin Adds plugin to client
224 | func (hhc *Client) AddPlugin(p heimdall.Plugin) {
225 | 	hhc.client.AddPlugin(p)
226 | }
227 | 


--------------------------------------------------------------------------------
/hystrix/hystrix_client_test.go:
--------------------------------------------------------------------------------
  1 | package hystrix
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"io/ioutil"
  6 | 	"net/http"
  7 | 	"net/http/httptest"
  8 | 	"strings"
  9 | 	"testing"
 10 | 	"time"
 11 | 
 12 | 	"github.com/gojek/heimdall/v7"
 13 | 	"github.com/stretchr/testify/assert"
 14 | 	"github.com/stretchr/testify/require"
 15 | )
 16 | 
 17 | type myHTTPClient struct {
 18 | 	client http.Client
 19 | }
 20 | 
 21 | func (c *myHTTPClient) Do(request *http.Request) (*http.Response, error) {
 22 | 	request.Header.Set("foo", "bar")
 23 | 	return c.client.Do(request)
 24 | }
 25 | 
 26 | func TestHystrixHTTPClientDoSuccess(t *testing.T) {
 27 | 	client := NewClient(
 28 | 		WithHTTPTimeout(50*time.Millisecond),
 29 | 		WithCommandName("some_command_name"),
 30 | 		WithHystrixTimeout(10*time.Millisecond),
 31 | 		WithMaxConcurrentRequests(100),
 32 | 		WithErrorPercentThreshold(10),
 33 | 		WithSleepWindow(100),
 34 | 		WithRequestVolumeThreshold(20),
 35 | 	)
 36 | 
 37 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
 38 | 		assert.Equal(t, http.MethodGet, r.Method)
 39 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
 40 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
 41 | 
 42 | 		w.WriteHeader(http.StatusOK)
 43 | 		_, _ = w.Write([]byte(`{ "response": "ok" }`))
 44 | 	}
 45 | 
 46 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
 47 | 	defer server.Close()
 48 | 
 49 | 	req, err := http.NewRequest(http.MethodGet, server.URL, nil)
 50 | 	require.NoError(t, err)
 51 | 	req.Header.Set("Content-Type", "application/json")
 52 | 	req.Header.Set("Accept-Language", "en")
 53 | 
 54 | 	response, err := client.Do(req)
 55 | 	require.NoError(t, err, "should not have failed to make a GET request")
 56 | 
 57 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
 58 | 	body, err := ioutil.ReadAll(response.Body)
 59 | 	require.NoError(t, err)
 60 | 	assert.Equal(t, "{ \"response\": \"ok\" }", string(body))
 61 | }
 62 | 
 63 | func TestHystrixHTTPClientGetSuccess(t *testing.T) {
 64 | 	client := NewClient(
 65 | 		WithHTTPTimeout(10*time.Millisecond),
 66 | 		WithCommandName("some_command_name"),
 67 | 		WithHystrixTimeout(10*time.Millisecond),
 68 | 		WithMaxConcurrentRequests(100),
 69 | 		WithErrorPercentThreshold(10),
 70 | 		WithSleepWindow(100),
 71 | 		WithRequestVolumeThreshold(10),
 72 | 	)
 73 | 
 74 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
 75 | 		assert.Equal(t, http.MethodGet, r.Method)
 76 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
 77 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
 78 | 
 79 | 		w.WriteHeader(http.StatusOK)
 80 | 		_, _ = w.Write([]byte(`{ "response": "ok" }`))
 81 | 	}
 82 | 
 83 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
 84 | 	defer server.Close()
 85 | 
 86 | 	headers := http.Header{}
 87 | 	headers.Set("Content-Type", "application/json")
 88 | 	headers.Set("Accept-Language", "en")
 89 | 
 90 | 	response, err := client.Get(server.URL, headers)
 91 | 	require.NoError(t, err, "should not have failed to make a GET request")
 92 | 
 93 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
 94 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
 95 | }
 96 | 
 97 | func TestHystrixHTTPClientPostSuccess(t *testing.T) {
 98 | 	client := NewClient(
 99 | 		WithHTTPTimeout(10*time.Millisecond),
100 | 		WithCommandName("some_command_name"),
101 | 		WithHystrixTimeout(10*time.Millisecond),
102 | 		WithMaxConcurrentRequests(100),
103 | 		WithErrorPercentThreshold(10),
104 | 		WithSleepWindow(100),
105 | 		WithRequestVolumeThreshold(10),
106 | 	)
107 | 
108 | 	requestBodyString := `{ "name": "heimdall" }`
109 | 
110 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
111 | 		assert.Equal(t, http.MethodPost, r.Method)
112 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
113 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
114 | 
115 | 		rBody, err := ioutil.ReadAll(r.Body)
116 | 		require.NoError(t, err, "should not have failed to extract request body")
117 | 
118 | 		assert.Equal(t, requestBodyString, string(rBody))
119 | 
120 | 		w.WriteHeader(http.StatusOK)
121 | 		_, _ = w.Write([]byte(`{ "response": "ok" }`))
122 | 	}
123 | 
124 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
125 | 	defer server.Close()
126 | 
127 | 	requestBody := bytes.NewReader([]byte(requestBodyString))
128 | 
129 | 	headers := http.Header{}
130 | 	headers.Set("Content-Type", "application/json")
131 | 	headers.Set("Accept-Language", "en")
132 | 
133 | 	response, err := client.Post(server.URL, requestBody, headers)
134 | 	require.NoError(t, err, "should not have failed to make a POST request")
135 | 
136 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
137 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
138 | }
139 | 
140 | func TestHystrixHTTPClientDeleteSuccess(t *testing.T) {
141 | 	client := NewClient(
142 | 		WithHTTPTimeout(10*time.Millisecond),
143 | 		WithCommandName("some_command_name"),
144 | 		WithHystrixTimeout(10*time.Millisecond),
145 | 		WithMaxConcurrentRequests(100),
146 | 		WithErrorPercentThreshold(10),
147 | 		WithSleepWindow(100),
148 | 		WithRequestVolumeThreshold(10),
149 | 	)
150 | 
151 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
152 | 		assert.Equal(t, http.MethodDelete, r.Method)
153 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
154 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
155 | 
156 | 		w.WriteHeader(http.StatusOK)
157 | 		_, _ = w.Write([]byte(`{ "response": "ok" }`))
158 | 	}
159 | 
160 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
161 | 	defer server.Close()
162 | 
163 | 	headers := http.Header{}
164 | 	headers.Set("Content-Type", "application/json")
165 | 	headers.Set("Accept-Language", "en")
166 | 
167 | 	response, err := client.Delete(server.URL, headers)
168 | 	require.NoError(t, err, "should not have failed to make a DELETE request")
169 | 
170 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
171 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
172 | }
173 | 
174 | func TestHystrixHTTPClientPutSuccess(t *testing.T) {
175 | 	client := NewClient(
176 | 		WithHTTPTimeout(10*time.Millisecond),
177 | 		WithCommandName("some_command_name"),
178 | 		WithHystrixTimeout(10*time.Millisecond),
179 | 		WithMaxConcurrentRequests(100),
180 | 		WithErrorPercentThreshold(10),
181 | 		WithSleepWindow(100),
182 | 		WithRequestVolumeThreshold(10),
183 | 	)
184 | 
185 | 	requestBodyString := `{ "name": "heimdall" }`
186 | 
187 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
188 | 		assert.Equal(t, http.MethodPut, r.Method)
189 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
190 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
191 | 
192 | 		rBody, err := ioutil.ReadAll(r.Body)
193 | 		require.NoError(t, err, "should not have failed to extract request body")
194 | 
195 | 		assert.Equal(t, requestBodyString, string(rBody))
196 | 
197 | 		w.WriteHeader(http.StatusOK)
198 | 		_, _ = w.Write([]byte(`{ "response": "ok" }`))
199 | 	}
200 | 
201 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
202 | 	defer server.Close()
203 | 
204 | 	requestBody := bytes.NewReader([]byte(requestBodyString))
205 | 
206 | 	headers := http.Header{}
207 | 	headers.Set("Content-Type", "application/json")
208 | 	headers.Set("Accept-Language", "en")
209 | 
210 | 	response, err := client.Put(server.URL, requestBody, headers)
211 | 	require.NoError(t, err, "should not have failed to make a PUT request")
212 | 
213 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
214 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
215 | }
216 | 
217 | func TestHystrixHTTPClientPatchSuccess(t *testing.T) {
218 | 	client := NewClient(
219 | 		WithHTTPTimeout(10*time.Millisecond),
220 | 		WithCommandName("some_command_name"),
221 | 		WithHystrixTimeout(10*time.Millisecond),
222 | 		WithMaxConcurrentRequests(100),
223 | 		WithErrorPercentThreshold(10),
224 | 		WithSleepWindow(100),
225 | 		WithRequestVolumeThreshold(10),
226 | 	)
227 | 
228 | 	requestBodyString := `{ "name": "heimdall" }`
229 | 
230 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
231 | 		assert.Equal(t, http.MethodPatch, r.Method)
232 | 		assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
233 | 		assert.Equal(t, "en", r.Header.Get("Accept-Language"))
234 | 
235 | 		rBody, err := ioutil.ReadAll(r.Body)
236 | 		require.NoError(t, err, "should not have failed to extract request body")
237 | 
238 | 		assert.Equal(t, requestBodyString, string(rBody))
239 | 
240 | 		w.WriteHeader(http.StatusOK)
241 | 		_, _ = w.Write([]byte(`{ "response": "ok" }`))
242 | 	}
243 | 
244 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
245 | 	defer server.Close()
246 | 
247 | 	headers := http.Header{}
248 | 	headers.Set("Content-Type", "application/json")
249 | 	headers.Set("Accept-Language", "en")
250 | 
251 | 	requestBody := bytes.NewReader([]byte(requestBodyString))
252 | 
253 | 	response, err := client.Patch(server.URL, requestBody, headers)
254 | 	require.NoError(t, err, "should not have failed to make a PATCH request")
255 | 
256 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
257 | 	assert.Equal(t, "{ \"response\": \"ok\" }", respBody(t, response))
258 | }
259 | 
260 | func TestHystrixHTTPClientRetriesGetOnFailure(t *testing.T) {
261 | 	backoffInterval := 1 * time.Millisecond
262 | 	maximumJitterInterval := 1 * time.Millisecond
263 | 
264 | 	client := NewClient(
265 | 		WithHTTPTimeout(10*time.Millisecond),
266 | 		WithCommandName("some_command_name"),
267 | 		WithHystrixTimeout(10*time.Millisecond),
268 | 		WithMaxConcurrentRequests(100),
269 | 		WithErrorPercentThreshold(10),
270 | 		WithSleepWindow(100),
271 | 		WithRequestVolumeThreshold(10),
272 | 		WithRetryCount(3),
273 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
274 | 	)
275 | 
276 | 	response, err := client.Get("url_doesnt_exist", http.Header{})
277 | 
278 | 	assert.Contains(t, err.Error(), "unsupported protocol scheme")
279 | 	assert.Nil(t, response)
280 | }
281 | 
282 | func TestHystrixHTTPClientRetriesGetOnFailure5xx(t *testing.T) {
283 | 	count := 0
284 | 	backoffInterval := 1 * time.Millisecond
285 | 	maximumJitterInterval := 1 * time.Millisecond
286 | 
287 | 	client := NewClient(
288 | 		WithHTTPTimeout(10*time.Millisecond),
289 | 		WithCommandName("some_command_name_5xx"),
290 | 		WithHystrixTimeout(10*time.Millisecond),
291 | 		WithMaxConcurrentRequests(100),
292 | 		WithErrorPercentThreshold(10),
293 | 		WithSleepWindow(100),
294 | 		WithRequestVolumeThreshold(10),
295 | 		WithRetryCount(3),
296 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
297 | 	)
298 | 
299 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
300 | 		w.WriteHeader(http.StatusInternalServerError)
301 | 		_, _ = w.Write([]byte(`{ "response": "something went wrong" }`))
302 | 		count = count + 1
303 | 	}
304 | 
305 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
306 | 	defer server.Close()
307 | 
308 | 	response, err := client.Get(server.URL, http.Header{})
309 | 	require.NoError(t, err)
310 | 
311 | 	assert.Equal(t, 4, count)
312 | 
313 | 	assert.Equal(t, http.StatusInternalServerError, response.StatusCode)
314 | 	assert.Equal(t, "{ \"response\": \"something went wrong\" }", respBody(t, response))
315 | }
316 | 
317 | func BenchmarkHystrixHTTPClientRetriesGetOnFailure(b *testing.B) {
318 | 	backoffInterval := 1 * time.Millisecond
319 | 	maximumJitterInterval := 1 * time.Millisecond
320 | 
321 | 	client := NewClient(
322 | 		WithHTTPTimeout(10*time.Millisecond),
323 | 		WithCommandName("some_command_name"),
324 | 		WithHystrixTimeout(10*time.Millisecond),
325 | 		WithMaxConcurrentRequests(100),
326 | 		WithErrorPercentThreshold(10),
327 | 		WithSleepWindow(100),
328 | 		WithRequestVolumeThreshold(10),
329 | 		WithRetryCount(3),
330 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
331 | 	)
332 | 
333 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
334 | 		w.WriteHeader(http.StatusInternalServerError)
335 | 		_, _ = w.Write([]byte(`{ "response": "something went wrong" }`))
336 | 	}
337 | 
338 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
339 | 	defer server.Close()
340 | 
341 | 	for i := 0; i < b.N; i++ {
342 | 		_, _ = client.Get(server.URL, http.Header{})
343 | 	}
344 | }
345 | 
346 | func TestHystrixHTTPClientRetriesPostOnFailure(t *testing.T) {
347 | 	count := 0
348 | 	backoffInterval := 1 * time.Millisecond
349 | 	maximumJitterInterval := 1 * time.Millisecond
350 | 
351 | 	client := NewClient(
352 | 		WithHTTPTimeout(50*time.Millisecond),
353 | 		WithCommandName("some_command_name"),
354 | 		WithHystrixTimeout(10*time.Millisecond),
355 | 		WithMaxConcurrentRequests(100),
356 | 		WithErrorPercentThreshold(10),
357 | 		WithSleepWindow(100),
358 | 		WithRequestVolumeThreshold(20),
359 | 		WithRetryCount(3),
360 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
361 | 	)
362 | 
363 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
364 | 		w.WriteHeader(http.StatusInternalServerError)
365 | 		_, _ = w.Write([]byte(`{ "response": "something went wrong" }`))
366 | 		count = count + 1
367 | 	}
368 | 
369 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
370 | 	defer server.Close()
371 | 
372 | 	response, err := client.Post(server.URL, strings.NewReader("a=1&b=2"), http.Header{})
373 | 	require.NoError(t, err)
374 | 
375 | 	assert.Equal(t, 4, count)
376 | 	assert.Equal(t, http.StatusInternalServerError, response.StatusCode)
377 | 	assert.JSONEq(t, `{ "response": "something went wrong" }`, respBody(t, response))
378 | }
379 | 
380 | func BenchmarkHystrixHTTPClientRetriesPostOnFailure(b *testing.B) {
381 | 	backoffInterval := 1 * time.Millisecond
382 | 	maximumJitterInterval := 1 * time.Millisecond
383 | 
384 | 	client := NewClient(
385 | 		WithHTTPTimeout(10*time.Millisecond),
386 | 		WithCommandName("some_command_name"),
387 | 		WithHystrixTimeout(10*time.Millisecond),
388 | 		WithMaxConcurrentRequests(100),
389 | 		WithErrorPercentThreshold(10),
390 | 		WithSleepWindow(100),
391 | 		WithRequestVolumeThreshold(10),
392 | 		WithRetryCount(3),
393 | 		WithRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(backoffInterval, maximumJitterInterval))),
394 | 	)
395 | 
396 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
397 | 		w.WriteHeader(http.StatusInternalServerError)
398 | 		_, _ = w.Write([]byte(`{ "response": "something went wrong" }`))
399 | 	}
400 | 
401 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
402 | 	defer server.Close()
403 | 
404 | 	for i := 0; i < b.N; i++ {
405 | 		_, _ = client.Post(server.URL, strings.NewReader("a=1&b=2"), http.Header{})
406 | 	}
407 | }
408 | 
409 | func TestHystrixHTTPClientReturnsFallbackFailureWithoutFallBackFunction(t *testing.T) {
410 | 	client := NewClient(
411 | 		WithHTTPTimeout(10*time.Millisecond),
412 | 		WithCommandName("some_command_name"),
413 | 		WithHystrixTimeout(10*time.Millisecond),
414 | 		WithMaxConcurrentRequests(100),
415 | 		WithErrorPercentThreshold(10),
416 | 		WithSleepWindow(100),
417 | 		WithRequestVolumeThreshold(10),
418 | 	)
419 | 
420 | 	_, err := client.Get("http://foobar.example", http.Header{})
421 | 	assert.Equal(t, err.Error(), "hystrix: circuit open")
422 | }
423 | 
424 | func TestHystrixHTTPClientReturnsFallbackFailureWithAFallBackFunctionWhichReturnAnError(t *testing.T) {
425 | 	client := NewClient(
426 | 		WithHTTPTimeout(10*time.Millisecond),
427 | 		WithCommandName("some_command_name"),
428 | 		WithHystrixTimeout(10*time.Millisecond),
429 | 		WithMaxConcurrentRequests(100),
430 | 		WithErrorPercentThreshold(10),
431 | 		WithSleepWindow(100),
432 | 		WithRequestVolumeThreshold(10),
433 | 		WithFallbackFunc(func(err error) error {
434 | 			// do something in the fallback function
435 | 			return err
436 | 		}),
437 | 	)
438 | 
439 | 	_, err := client.Get("http://foobar.example", http.Header{})
440 | 	require.Error(t, err, "should have failed")
441 | 
442 | 	assert.True(t, strings.Contains(err.Error(), "fallback failed"))
443 | }
444 | 
445 | func TestFallBackFunctionIsCalledWithHystrixHTTPClient(t *testing.T) {
446 | 	called := false
447 | 
448 | 	client := NewClient(
449 | 		WithHTTPTimeout(10*time.Millisecond),
450 | 		WithCommandName("some_command_name"),
451 | 		WithHystrixTimeout(10*time.Millisecond),
452 | 		WithMaxConcurrentRequests(100),
453 | 		WithErrorPercentThreshold(10),
454 | 		WithSleepWindow(100),
455 | 		WithRequestVolumeThreshold(10),
456 | 		WithFallbackFunc(func(err error) error {
457 | 			called = true
458 | 			return err
459 | 		}),
460 | 	)
461 | 
462 | 	_, err := client.Get("http://foobar.example", http.Header{})
463 | 	require.Error(t, err, "should have failed")
464 | 
465 | 	assert.True(t, called)
466 | }
467 | 
468 | func TestHystrixHTTPClientReturnsFallbackFailureWithAFallBackFunctionWhichReturnsNil(t *testing.T) {
469 | 	client := NewClient(
470 | 		WithHTTPTimeout(10*time.Millisecond),
471 | 		WithCommandName("some_command_name"),
472 | 		WithHystrixTimeout(10*time.Millisecond),
473 | 		WithMaxConcurrentRequests(100),
474 | 		WithErrorPercentThreshold(10),
475 | 		WithSleepWindow(100),
476 | 		WithRequestVolumeThreshold(10),
477 | 		WithFallbackFunc(func(err error) error {
478 | 			// do something in the fallback function
479 | 			return nil
480 | 		}),
481 | 	)
482 | 
483 | 	_, err := client.Get("http://foobar.example", http.Header{})
484 | 	assert.Nil(t, err)
485 | }
486 | 
487 | func TestCustomHystrixHTTPClientDoSuccess(t *testing.T) {
488 | 	client := NewClient(
489 | 		WithHTTPTimeout(10*time.Millisecond),
490 | 		WithCommandName("some_new_command_name"),
491 | 		WithHystrixTimeout(10*time.Millisecond),
492 | 		WithMaxConcurrentRequests(100),
493 | 		WithErrorPercentThreshold(10),
494 | 		WithSleepWindow(100),
495 | 		WithRequestVolumeThreshold(10),
496 | 		WithHTTPClient(&myHTTPClient{
497 | 			client: http.Client{Timeout: 25 * time.Millisecond}}),
498 | 	)
499 | 
500 | 	dummyHandler := func(w http.ResponseWriter, r *http.Request) {
501 | 		assert.Equal(t, r.Header.Get("foo"), "bar")
502 | 		assert.NotEqual(t, r.Header.Get("foo"), "baz")
503 | 		w.WriteHeader(http.StatusOK)
504 | 		_, _ = w.Write([]byte(`{ "response": "ok" }`))
505 | 	}
506 | 
507 | 	server := httptest.NewServer(http.HandlerFunc(dummyHandler))
508 | 	defer server.Close()
509 | 
510 | 	req, err := http.NewRequest(http.MethodGet, server.URL, nil)
511 | 	require.NoError(t, err)
512 | 	response, err := client.Do(req)
513 | 	assert.Equal(t, http.StatusOK, response.StatusCode)
514 | 
515 | 	body, err := ioutil.ReadAll(response.Body)
516 | 	require.NoError(t, err)
517 | 	assert.Equal(t, "{ \"response\": \"ok\" }", string(body))
518 | }
519 | 
520 | func respBody(t *testing.T, response *http.Response) string {
521 | 	if response.Body != nil {
522 | 		defer func() {
523 | 			_ = response.Body.Close()
524 | 		}()
525 | 	}
526 | 
527 | 	respBody, err := ioutil.ReadAll(response.Body)
528 | 	require.NoError(t, err, "should not have failed to read response body")
529 | 
530 | 	return string(respBody)
531 | }
532 | 
533 | func TestDurationToInt(t *testing.T) {
534 | 	t.Run("1sec should return 1 when unit is second", func(t *testing.T) {
535 | 		timeout := 1 * time.Second
536 | 		timeoutInSec := durationToInt(timeout, time.Second)
537 | 
538 | 		assert.Equal(t, 1, timeoutInSec)
539 | 	})
540 | 
541 | 	t.Run("30sec should return 30000 when unit is millisecond", func(t *testing.T) {
542 | 		timeout := 30 * time.Second
543 | 		timeoutInMs := durationToInt(timeout, time.Millisecond)
544 | 
545 | 		assert.Equal(t, 30000, timeoutInMs)
546 | 	})
547 | }
548 | 


--------------------------------------------------------------------------------
/hystrix/options.go:
--------------------------------------------------------------------------------
 1 | package hystrix
 2 | 
 3 | import (
 4 | 	"time"
 5 | 
 6 | 	"github.com/afex/hystrix-go/plugins"
 7 | 	"github.com/gojek/heimdall/v7"
 8 | 	"github.com/gojek/heimdall/v7/httpclient"
 9 | )
10 | 
11 | // Option represents the hystrix client options
12 | type Option func(*Client)
13 | 
14 | // WithCommandName sets the hystrix command name
15 | func WithCommandName(name string) Option {
16 | 	return func(c *Client) {
17 | 		c.hystrixCommandName = name
18 | 	}
19 | }
20 | 
21 | // WithHTTPTimeout sets hystrix timeout
22 | func WithHTTPTimeout(timeout time.Duration) Option {
23 | 	return func(c *Client) {
24 | 		c.timeout = timeout
25 | 	}
26 | }
27 | 
28 | // WithHystrixTimeout sets hystrix timeout
29 | func WithHystrixTimeout(timeout time.Duration) Option {
30 | 	return func(c *Client) {
31 | 		c.hystrixTimeout = timeout
32 | 	}
33 | }
34 | 
35 | // WithMaxConcurrentRequests sets hystrix max concurrent requests
36 | func WithMaxConcurrentRequests(maxConcurrentRequests int) Option {
37 | 	return func(c *Client) {
38 | 		c.maxConcurrentRequests = maxConcurrentRequests
39 | 	}
40 | }
41 | 
42 | // WithRequestVolumeThreshold sets hystrix request volume threshold
43 | func WithRequestVolumeThreshold(requestVolumeThreshold int) Option {
44 | 	return func(c *Client) {
45 | 		c.requestVolumeThreshold = requestVolumeThreshold
46 | 	}
47 | }
48 | 
49 | // WithSleepWindow sets hystrix sleep window
50 | func WithSleepWindow(sleepWindow int) Option {
51 | 	return func(c *Client) {
52 | 		c.sleepWindow = sleepWindow
53 | 	}
54 | }
55 | 
56 | // WithErrorPercentThreshold sets hystrix error percent threshold
57 | func WithErrorPercentThreshold(errorPercentThreshold int) Option {
58 | 	return func(c *Client) {
59 | 		c.errorPercentThreshold = errorPercentThreshold
60 | 	}
61 | }
62 | 
63 | // WithFallbackFunc sets the fallback function
64 | func WithFallbackFunc(fn fallbackFunc) Option {
65 | 	return func(c *Client) {
66 | 		c.fallbackFunc = fn
67 | 	}
68 | }
69 | 
70 | // WithRetryCount sets the retry count for the Client
71 | func WithRetryCount(retryCount int) Option {
72 | 	return func(c *Client) {
73 | 		c.retryCount = retryCount
74 | 	}
75 | }
76 | 
77 | // WithRetrier sets the strategy for retrying
78 | func WithRetrier(retrier heimdall.Retriable) Option {
79 | 	return func(c *Client) {
80 | 		c.retrier = retrier
81 | 	}
82 | }
83 | 
84 | // WithHTTPClient sets a custom http client for hystrix client
85 | func WithHTTPClient(client heimdall.Doer) Option {
86 | 	return func(c *Client) {
87 | 		opt := httpclient.WithHTTPClient(client)
88 | 		opt(c.client)
89 | 	}
90 | }
91 | 
92 | // WithStatsDCollector exports hystrix metrics to a statsD backend
93 | func WithStatsDCollector(addr, prefix string) Option {
94 | 	return func(c *Client) {
95 | 		c.statsD = &plugins.StatsdCollectorConfig{StatsdAddr: addr, Prefix: prefix}
96 | 	}
97 | }
98 | 


--------------------------------------------------------------------------------
/hystrix/options_test.go:
--------------------------------------------------------------------------------
  1 | package hystrix
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"net/http"
  6 | 	"testing"
  7 | 	"time"
  8 | 
  9 | 	"github.com/stretchr/testify/assert"
 10 | )
 11 | 
 12 | func TestOptionsAreSet(t *testing.T) {
 13 | 	c := NewClient(
 14 | 		WithHTTPTimeout(10*time.Second),
 15 | 		WithCommandName("test"),
 16 | 		WithHystrixTimeout(1100),
 17 | 		WithMaxConcurrentRequests(10),
 18 | 		WithErrorPercentThreshold(30),
 19 | 		WithSleepWindow(5),
 20 | 		WithRequestVolumeThreshold(5),
 21 | 		WithStatsDCollector("localhost:8125", "myapp.hystrix"),
 22 | 	)
 23 | 
 24 | 	assert.Equal(t, 10*time.Second, c.timeout)
 25 | 	assert.Equal(t, "test", c.hystrixCommandName)
 26 | 	assert.Equal(t, time.Duration(1100), c.hystrixTimeout)
 27 | 	assert.Equal(t, 10, c.maxConcurrentRequests)
 28 | 	assert.Equal(t, 30, c.errorPercentThreshold)
 29 | 	assert.Equal(t, 5, c.sleepWindow)
 30 | 	assert.Equal(t, 5, c.requestVolumeThreshold)
 31 | 	assert.Equal(t, "localhost:8125", c.statsD.StatsdAddr)
 32 | 	assert.Equal(t, "myapp.hystrix", c.statsD.Prefix)
 33 | }
 34 | 
 35 | func TestOptionsHaveDefaults(t *testing.T) {
 36 | 	c := NewClient(WithCommandName("test-defaults"))
 37 | 
 38 | 	assert.Equal(t, 30*time.Second, c.timeout)
 39 | 	assert.Equal(t, "test-defaults", c.hystrixCommandName)
 40 | 	assert.Equal(t, 30*time.Second, c.hystrixTimeout)
 41 | 	assert.Equal(t, 100, c.maxConcurrentRequests)
 42 | 	assert.Equal(t, 25, c.errorPercentThreshold)
 43 | 	assert.Equal(t, 10, c.sleepWindow)
 44 | 	assert.Equal(t, 10, c.requestVolumeThreshold)
 45 | 	assert.Nil(t, c.statsD)
 46 | }
 47 | 
 48 | func ExampleWithHTTPTimeout() {
 49 | 	c := NewClient(WithHTTPTimeout(5 * time.Second))
 50 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
 51 | 	if err != nil {
 52 | 		panic(err)
 53 | 	}
 54 | 	res, err := c.Do(req)
 55 | 	if err != nil {
 56 | 		panic(err)
 57 | 	}
 58 | 	fmt.Println("Response status : ", res.StatusCode)
 59 | 	// Output: Response status :  200
 60 | }
 61 | 
 62 | func ExampleWithHTTPTimeout_expired() {
 63 | 	c := NewClient(WithHTTPTimeout(1 * time.Millisecond))
 64 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
 65 | 	if err != nil {
 66 | 		panic(err)
 67 | 	}
 68 | 	res, err := c.Do(req)
 69 | 	if err != nil {
 70 | 		fmt.Println("error:", err)
 71 | 		return
 72 | 	}
 73 | 	fmt.Println("Response status : ", res.StatusCode)
 74 | }
 75 | 
 76 | func ExampleWithRetryCount() {
 77 | 	c := NewClient(WithHTTPTimeout(1*time.Millisecond), WithRetryCount(3))
 78 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
 79 | 	if err != nil {
 80 | 		panic(err)
 81 | 	}
 82 | 	res, err := c.Do(req)
 83 | 	if err != nil {
 84 | 		fmt.Println("error:", err)
 85 | 		return
 86 | 	}
 87 | 	fmt.Println("Response status : ", res.StatusCode)
 88 | }
 89 | 
 90 | type mockClient struct{}
 91 | 
 92 | func (m *mockClient) Do(r *http.Request) (*http.Response, error) {
 93 | 	fmt.Println("mock client called")
 94 | 	return &http.Response{}, nil
 95 | }
 96 | 
 97 | func ExampleWithHTTPClient() {
 98 | 	m := &mockClient{}
 99 | 	c := NewClient(WithHTTPClient(m))
100 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
101 | 	if err != nil {
102 | 		panic(err)
103 | 	}
104 | 	_, _ = c.Do(req)
105 | 	// Output: mock client called
106 | }
107 | 
108 | type mockRetrier struct{}
109 | 
110 | func (m *mockRetrier) NextInterval(attempt int) time.Duration {
111 | 	fmt.Println("retry attempt", attempt)
112 | 	return time.Millisecond
113 | }
114 | 
115 | func ExampleWithRetrier() {
116 | 	c := NewClient(WithHTTPTimeout(1*time.Millisecond), WithRetryCount(3), WithRetrier(&mockRetrier{}))
117 | 	req, err := http.NewRequest(http.MethodGet, "https://www.link.doesnt.exist.io/", nil)
118 | 	if err != nil {
119 | 		panic(err)
120 | 	}
121 | 	res, err := c.Do(req)
122 | 	if err != nil {
123 | 		fmt.Println("error")
124 | 		return
125 | 	}
126 | 	fmt.Println("Response status : ", res.StatusCode)
127 | 	// Output: retry attempt 0
128 | 	// retry attempt 1
129 | 	// retry attempt 2
130 | 	// retry attempt 3
131 | 	// error
132 | }
133 | 
134 | func ExampleWithStatsDCollector() {
135 | 	c := NewClient(WithStatsDCollector("localhost:8125", "myapp.hystrix"))
136 | 	req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
137 | 	if err != nil {
138 | 		panic(err)
139 | 	}
140 | 	res, err := c.Do(req)
141 | 	if err != nil {
142 | 		panic(err)
143 | 	}
144 | 	fmt.Println("Response status : ", res.StatusCode)
145 | 	// Output: Response status :  200
146 | }
147 | 


--------------------------------------------------------------------------------
/plugin.go:
--------------------------------------------------------------------------------
 1 | package heimdall
 2 | 
 3 | import (
 4 | 	"net/http"
 5 | )
 6 | 
 7 | // Plugin defines the interface that a Heimdall plugin must have
 8 | // plugins can be added to a Heimdall client using the `AddPlugin` method
 9 | type Plugin interface {
10 | 	OnRequestStart(*http.Request)
11 | 	OnRequestEnd(*http.Request, *http.Response)
12 | 	OnError(*http.Request, error)
13 | }
14 | 


--------------------------------------------------------------------------------
/plugins/request_logger.go:
--------------------------------------------------------------------------------
 1 | package plugins
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 	"io"
 7 | 	"net/http"
 8 | 	"os"
 9 | 	"time"
10 | 
11 | 	"github.com/gojek/heimdall/v7"
12 | )
13 | 
14 | type ctxKey string
15 | 
16 | const reqTime ctxKey = "request_time_start"
17 | 
18 | type requestLogger struct {
19 | 	out    io.Writer
20 | 	errOut io.Writer
21 | }
22 | 
23 | // NewRequestLogger returns a new instance of a Heimdall request logger plugin
24 | // out and errOut are the streams where standard and error logs are written respectively
25 | // If given as nil, `out` takes the default value of `os.StdOut`
26 | // and errOut takes the default value of `os.StdErr`
27 | func NewRequestLogger(out io.Writer, errOut io.Writer) heimdall.Plugin {
28 | 	if out == nil {
29 | 		out = os.Stdout
30 | 	}
31 | 	if errOut == nil {
32 | 		errOut = os.Stderr
33 | 	}
34 | 	return &requestLogger{
35 | 		out:    out,
36 | 		errOut: errOut,
37 | 	}
38 | }
39 | 
40 | func (rl *requestLogger) OnRequestStart(req *http.Request) {
41 | 	ctx := context.WithValue(req.Context(), reqTime, time.Now())
42 | 	*req = *(req.WithContext(ctx))
43 | }
44 | 
45 | func (rl *requestLogger) OnRequestEnd(req *http.Request, res *http.Response) {
46 | 	reqDurationMs := getRequestDuration(req.Context()) / time.Millisecond
47 | 	method := req.Method
48 | 	url := req.URL.String()
49 | 	statusCode := res.StatusCode
50 | 	fmt.Fprintf(rl.out, "%s %s %s %d [%dms]\n", time.Now().Format("02/Jan/2006 03:04:05"), method, url, statusCode, reqDurationMs)
51 | }
52 | 
53 | func (rl *requestLogger) OnError(req *http.Request, err error) {
54 | 	reqDurationMs := getRequestDuration(req.Context()) / time.Millisecond
55 | 	method := req.Method
56 | 	url := req.URL.String()
57 | 	fmt.Fprintf(rl.errOut, "%s %s %s [%dms] ERROR: %v\n", time.Now().Format("02/Jan/2006 03:04:05"), method, url, reqDurationMs, err)
58 | }
59 | 
60 | func getRequestDuration(ctx context.Context) time.Duration {
61 | 	now := time.Now()
62 | 	start := ctx.Value(reqTime)
63 | 	if start == nil {
64 | 		return 0
65 | 	}
66 | 	startTime, ok := start.(time.Time)
67 | 	if !ok {
68 | 		return 0
69 | 	}
70 | 	return now.Sub(startTime)
71 | }
72 | 


--------------------------------------------------------------------------------
/retry.go:
--------------------------------------------------------------------------------
 1 | package heimdall
 2 | 
 3 | import "time"
 4 | 
 5 | // Retriable defines contract for retriers to implement
 6 | type Retriable interface {
 7 | 	NextInterval(retry int) time.Duration
 8 | }
 9 | 
10 | // RetriableFunc is an adapter to allow the use of ordinary functions
11 | // as a Retriable
12 | type RetriableFunc func(retry int) time.Duration
13 | 
14 | // NextInterval calls f(retry)
15 | func (f RetriableFunc) NextInterval(retry int) time.Duration {
16 | 	return f(retry)
17 | }
18 | 
19 | type retrier struct {
20 | 	backoff Backoff
21 | }
22 | 
23 | // NewRetrier returns retrier with some backoff strategy
24 | func NewRetrier(backoff Backoff) Retriable {
25 | 	return &retrier{
26 | 		backoff: backoff,
27 | 	}
28 | }
29 | 
30 | // NewRetrierFunc returns a retrier with a retry function defined
31 | func NewRetrierFunc(f RetriableFunc) Retriable {
32 | 	return f
33 | }
34 | 
35 | // NextInterval returns next retriable time
36 | func (r *retrier) NextInterval(retry int) time.Duration {
37 | 	return r.backoff.Next(retry)
38 | }
39 | 
40 | type noRetrier struct {
41 | }
42 | 
43 | // NewNoRetrier returns a null object for retriable
44 | func NewNoRetrier() Retriable {
45 | 	return &noRetrier{}
46 | }
47 | 
48 | // NextInterval returns next retriable time, always 0
49 | func (r *noRetrier) NextInterval(retry int) time.Duration {
50 | 	return 0 * time.Millisecond
51 | }
52 | 


--------------------------------------------------------------------------------
/retry_test.go:
--------------------------------------------------------------------------------
 1 | package heimdall
 2 | 
 3 | import (
 4 | 	"testing"
 5 | 	"time"
 6 | 
 7 | 	"github.com/stretchr/testify/assert"
 8 | )
 9 | 
10 | func TestRetrierWithExponentialBackoff(t *testing.T) {
11 | 
12 | 	exponentialBackoff := NewExponentialBackoff(2*time.Millisecond, 10*time.Millisecond, 2.0, 1*time.Millisecond)
13 | 	exponentialRetrier := NewRetrier(exponentialBackoff)
14 | 
15 | 	assert.True(t, 4*time.Millisecond <= exponentialRetrier.NextInterval(1))
16 | }
17 | 
18 | func TestRetrierWithConstantBackoff(t *testing.T) {
19 | 	backoffInterval := 2 * time.Millisecond
20 | 	maximumJitterInterval := 1 * time.Millisecond
21 | 
22 | 	constantBackoff := NewConstantBackoff(backoffInterval, maximumJitterInterval)
23 | 	constantRetrier := NewRetrier(constantBackoff)
24 | 
25 | 	assert.True(t, 2*time.Millisecond <= constantRetrier.NextInterval(1))
26 | }
27 | 
28 | func TestRetrierFunc(t *testing.T) {
29 | 	linearRetrier := NewRetrierFunc(func(retry int) time.Duration {
30 | 		if retry <= 0 {
31 | 			return 0 * time.Millisecond
32 | 		}
33 | 		return time.Duration(retry) * time.Millisecond
34 | 	})
35 | 
36 | 	assert.True(t, 3*time.Millisecond <= linearRetrier.NextInterval(4))
37 | }
38 | 
39 | func TestNoRetrier(t *testing.T) {
40 | 	noRetrier := NewNoRetrier()
41 | 	nextInterval := noRetrier.NextInterval(1)
42 | 	assert.Equal(t, time.Duration(0), nextInterval)
43 | }
44 | 


--------------------------------------------------------------------------------