├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── go.mod ├── go.sum └── reliablereq ├── default.go ├── reliablereq.go └── reliablereq_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | vendor 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.12.x" 4 | 5 | env: 6 | - GO111MODULE=on 7 | 8 | install: true 9 | 10 | notifications: 11 | email: false 12 | 13 | script: go test -v ./... 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.0.2] - 2020-08-27 10 | ### Changed 11 | - Expose cache data 12 | 13 | ## [0.0.1] - 2019-07-16 14 | ### Added 15 | - Use histryx-go as circuit breaker 16 | - Add cache feature 17 | - Add stale feature 18 | 19 | [Unreleased]: https://github.com/globocom/reliable-request/compare/v0.0.1...HEAD 20 | [0.0.1]: https://github.com/globocom/reliable-request/releases/tag/v0.0.1 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Globo.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reliablereq 2 | [![Build Status](https://travis-ci.org/globocom/reliable-request.svg?branch=master)](https://travis-ci.org/globocom/reliable-request) [![Go Report Card](https://goreportcard.com/badge/github.com/globocom/reliable-request)](https://goreportcard.com/report/github.com/globocom/reliable-request) 3 | 4 | A golang opinionated library to provide reliable request using [hystrix-go](https://github.com/afex/hystrix-go), [go-cache](https://github.com/patrickmn/go-cache), and [go-resiliency](https://github.com/eapache/go-resiliency). 5 | 6 | When you do a `Get`, it provides: 7 | * an HTTP client configured with timeouts and [keepalive](https://en.wikipedia.org/wiki/HTTP_persistent_connection), 8 | * a [circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html) and 9 | * a proper [caching system](https://en.wikipedia.org/wiki/Cache_(computing)). 10 | 11 | # Usage 12 | 13 | ```golang 14 | req := reliablereq.NewReliableRequest() 15 | req.TTLCache = 1 * time.Second 16 | req.EnableStaleCache = false 17 | body, err := req.Get("http://example.com/list") 18 | 19 | // passing authentication/authorization bearer token 20 | req := reliablereq.NewReliableRequest() 21 | req.Headers = map[string]string{"Authorization": "Bearer foobar"} 22 | body, err := req.Get("http://example.com/list") 23 | 24 | // creating a different hystrix command 25 | req := reliablereq.NewReliableRequest() 26 | req.UpdateHystrixConfig("api2command", hystrix.CommandConfig{ 27 | Timeout: 800 + 100, 28 | MaxConcurrentRequests: 100, 29 | ErrorPercentThreshold: 50, 30 | RequestVolumeThreshold: 20, 31 | SleepWindow: 5000, 32 | }) 33 | body, err := req.Get("http://example.com/list") 34 | ``` 35 | 36 | ## WARNING 37 | Make sure you use different Hystrix commands for other endpoint APIs or separated Circuit Breaker contexts, otherwise, an endpoint may open the circuit breaker and all other requests will fail. 38 | 39 | 40 | # Opinionated defaults 41 | 42 | ```golang 43 | // reliable request defaults 44 | rr := ReliableRequest{ 45 | EnableCache: true, 46 | TTLCache: 1 * time.Minute, 47 | EnableStaleCache: true, 48 | TTLStaleCache: 24 * time.Hour, 49 | } 50 | // hystrix 51 | var defaultHystrixConfiguration = hystrix.CommandConfig{ 52 | Timeout: 800 + 100, // the defaultTimeout http client + a small gap 53 | MaxConcurrentRequests: 100, 54 | ErrorPercentThreshold: 50, 55 | RequestVolumeThreshold: 3, 56 | SleepWindow: 5000, 57 | } 58 | // http client 59 | client := &http.Client{ 60 | Transport: &http.Transport{ 61 | DialContext: (&net.Dialer{ 62 | Timeout: 800 * time.Millisecond, 63 | KeepAlive: 30 * time.Second, 64 | }).DialContext, 65 | MaxIdleConns: 100, 66 | MaxIdleConnsPerHost: 100, 67 | TLSHandshakeTimeout: 800 * time.Millisecond, 68 | }, 69 | Timeout: 800 * time.Millisecond, 70 | } 71 | ``` 72 | 73 | # Future 74 | 75 | * provide a proxy to setup hystrix 76 | * add retry logic (by go-resiliency) 77 | * add more examples, like token header requests and more 78 | * discuss the adopted defaults 79 | * discuss whether async hystrix is better (Go instead of Do) 80 | * understand and test the simultaneous client req hystrix config to see its implications 81 | * add go api documentation 82 | * add hooks (callbacks) to provides means for metrics gathering 83 | * add more HTTP verbs? 84 | * add load stress 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/globocom/reliable-request 2 | 3 | require ( 4 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 5 | github.com/imdario/mergo v0.3.7 6 | github.com/patrickmn/go-cache v2.1.0+incompatible 7 | github.com/pkg/errors v0.8.1 8 | github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 // indirect 9 | github.com/stretchr/testify v1.3.0 10 | gopkg.in/h2non/gock.v1 v1.0.15 11 | gopkg.in/yaml.v2 v2.2.2 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= 2 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 6 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 7 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 8 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 9 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 10 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 11 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 12 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 13 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= 14 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= 15 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 16 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 17 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 18 | github.com/pkg/errors v0.8.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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 22 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 23 | github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI= 24 | github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 27 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 28 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 29 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 30 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 32 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= 36 | gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= 37 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 38 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 39 | -------------------------------------------------------------------------------- /reliablereq/default.go: -------------------------------------------------------------------------------- 1 | package reliablereq 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/afex/hystrix-go/hystrix" 9 | ) 10 | 11 | const defaultTCPConnectionTimeout = 500 * time.Millisecond 12 | const defaultTimeout = 800 * time.Millisecond 13 | const defaultKeepAliveConnections = 100 14 | const defaultCommandName = "default_config" 15 | 16 | func timeoutHTTPClient() *http.Client { 17 | client := &http.Client{ 18 | Transport: &http.Transport{ 19 | DialContext: (&net.Dialer{ 20 | Timeout: defaultTCPConnectionTimeout, 21 | KeepAlive: 30 * time.Second, 22 | }).DialContext, 23 | MaxIdleConns: defaultKeepAliveConnections, 24 | MaxIdleConnsPerHost: defaultKeepAliveConnections, 25 | TLSHandshakeTimeout: defaultTCPConnectionTimeout, 26 | }, 27 | Timeout: defaultTimeout, 28 | } 29 | return client 30 | } 31 | 32 | func makeDefaultHistryx() { 33 | var defaultHystrixConfiguration = hystrix.CommandConfig{ 34 | Timeout: 800 + 100, // the defaultTimeout http client + a small gap 35 | MaxConcurrentRequests: 100, 36 | ErrorPercentThreshold: 50, 37 | RequestVolumeThreshold: 20, 38 | SleepWindow: 5000, 39 | } 40 | 41 | hystrix.ConfigureCommand(defaultCommandName, defaultHystrixConfiguration) 42 | } 43 | 44 | func defaultReliableRequest() ReliableRequest { 45 | rr := ReliableRequest{ 46 | HTTPClient: timeoutHTTPClient(), 47 | EnableCache: true, 48 | TTLCache: 1 * time.Minute, 49 | EnableStaleCache: true, 50 | TTLStaleCache: 24 * time.Hour, 51 | hystrixCommandName: defaultCommandName, 52 | } 53 | return rr 54 | } 55 | -------------------------------------------------------------------------------- /reliablereq/reliablereq.go: -------------------------------------------------------------------------------- 1 | package reliablereq 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/afex/hystrix-go/hystrix" 10 | "github.com/patrickmn/go-cache" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | var cachedResponses *cache.Cache 15 | 16 | func init() { 17 | makeDefaultHistryx() 18 | cachedResponses = cache.New(5*time.Minute, 10*time.Minute) 19 | } 20 | 21 | func Flush() { 22 | cachedResponses.Flush() 23 | hystrix.Flush() 24 | } 25 | 26 | // ReliableRequest - a struct holding params to make reliable requests 27 | type ReliableRequest struct { 28 | Headers map[string]string 29 | HTTPClient *http.Client 30 | EnableCache bool 31 | TTLCache time.Duration 32 | EnableStaleCache bool 33 | TTLStaleCache time.Duration 34 | hystrixCommandName string 35 | } 36 | 37 | // NewReliableRequest - create a new ReliableRequest 38 | func NewReliableRequest() *ReliableRequest { 39 | rr := defaultReliableRequest() 40 | 41 | return &rr 42 | } 43 | 44 | // Get - returns the requested data as string and a possible error 45 | func (rr *ReliableRequest) Get(url string) (string, error) { 46 | var body string 47 | 48 | if rr.EnableCache { 49 | cached, found := rr.GetCache(url) 50 | if found { 51 | return cached, nil 52 | } 53 | } 54 | 55 | err := hystrix.Do(rr.hystrixCommandName, func() error { 56 | resp, err := rr.urlRequest(url) 57 | if err == nil { 58 | rawBody, _ := ioutil.ReadAll(resp.Body) 59 | body = string(rawBody) 60 | rr.setCache(url, body) 61 | } 62 | return err 63 | }, func(previousError error) error { 64 | var err error 65 | 66 | if rr.EnableStaleCache { 67 | body, err = rr.getStaleCache(url) 68 | if err != nil { 69 | return errors.Wrap(previousError, err.Error()) 70 | } 71 | } else { 72 | return previousError 73 | } 74 | 75 | return nil 76 | }) 77 | 78 | if err != nil { 79 | return "", errors.Wrap(err, fmt.Sprintf("could not complete the request for url")) 80 | } 81 | return body, nil 82 | } 83 | 84 | //UpdateHystrixConfig - configure a new circuit breaker 85 | func (rr *ReliableRequest) UpdateHystrixConfig(name string, conf hystrix.CommandConfig) { 86 | rr.hystrixCommandName = name 87 | hystrix.ConfigureCommand(name, conf) 88 | } 89 | 90 | func keyStale(key string) string { 91 | return fmt.Sprintf("%s-stale", key) 92 | } 93 | 94 | func (rr *ReliableRequest) setCache(url, body string) { 95 | cachedResponses.Set(url, body, rr.TTLCache) 96 | 97 | if rr.EnableCache && rr.EnableStaleCache { 98 | cachedResponses.Set(keyStale(url), body, rr.TTLStaleCache) 99 | } 100 | } 101 | 102 | func (rr *ReliableRequest) GetCache(key string) (string, bool) { 103 | cached, found := cachedResponses.Get(key) 104 | if found { 105 | return cached.(string), true 106 | } 107 | 108 | return "", false 109 | } 110 | 111 | func (rr *ReliableRequest) getStaleCache(key string) (string, error) { 112 | cached, found := rr.GetCache(keyStale(key)) 113 | if found { 114 | return cached, nil 115 | } 116 | return "", errors.New("failed to fetch stale response") 117 | } 118 | 119 | func (rr *ReliableRequest) urlRequest(url string) (*http.Response, error) { 120 | req, err := http.NewRequest("GET", url, nil) 121 | if err != nil { 122 | return nil, errors.Wrap(err, fmt.Sprintf("could not create an http request for url: %s", url)) 123 | } 124 | 125 | if rr.Headers != nil { 126 | for k, v := range rr.Headers { 127 | req.Header.Add(k, v) 128 | } 129 | } 130 | resp, err := rr.HTTPClient.Do(req) 131 | 132 | if err == nil { 133 | // we only cache 2xx 134 | if resp.StatusCode != http.StatusOK { 135 | return nil, errors.New(fmt.Sprintf("bad response: %s for url: %s", resp.Status, url)) 136 | } 137 | } 138 | 139 | return resp, err 140 | } 141 | -------------------------------------------------------------------------------- /reliablereq/reliablereq_test.go: -------------------------------------------------------------------------------- 1 | package reliablereq 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/afex/hystrix-go/hystrix" 8 | "github.com/stretchr/testify/assert" 9 | gock "gopkg.in/h2non/gock.v1" 10 | ) 11 | 12 | func Test_It_returns_a_valid_response(t *testing.T) { 13 | defer gock.Off() 14 | Flush() 15 | 16 | gock.New("http://example.com"). 17 | Get("/list"). 18 | Reply(200). 19 | JSON(map[string]interface{}{"name": "mock"}) 20 | 21 | req := NewReliableRequest() 22 | // we need to intercept current http client due 23 | // https://github.com/h2non/gock/issues/27#issuecomment-334177773 24 | gock.InterceptClient(req.HTTPClient) 25 | defer gock.RestoreClient(req.HTTPClient) 26 | 27 | body, err := req.Get("http://example.com/list") 28 | 29 | assert.Nil(t, err) 30 | assert.NotNil(t, body) 31 | assert.Equal(t, "{\"name\":\"mock\"}\n", body) 32 | } 33 | 34 | func Test_It_raises_an_error_when_there_is_no_connection(t *testing.T) { 35 | Flush() 36 | 37 | req := NewReliableRequest() 38 | 39 | _, err := req.Get("http://example.non/list") 40 | 41 | assert.NotNil(t, err) 42 | } 43 | 44 | func Test_It_returns_an_error_when_server_responds_with_a_non_2xx(t *testing.T) { 45 | 46 | defer gock.Off() 47 | Flush() 48 | 49 | non2xx := []int{400, 404, 503} 50 | for _, status := range non2xx { 51 | gock.New("http://example.com"). 52 | Get("/list"). 53 | Reply(status) 54 | 55 | req := NewReliableRequest() 56 | gock.InterceptClient(req.HTTPClient) 57 | defer gock.RestoreClient(req.HTTPClient) 58 | 59 | _, err := req.Get("http://example.com/list") 60 | 61 | assert.NotNil(t, err) 62 | } 63 | } 64 | 65 | func Test_It_uses_cache_when_enabled(t *testing.T) { 66 | defer gock.Off() 67 | Flush() 68 | 69 | gock.New("http://example.com"). 70 | Get("/list"). 71 | Times(1). 72 | Reply(200). 73 | JSON(map[string]interface{}{"name": "mock"}) 74 | 75 | req := NewReliableRequest() 76 | gock.InterceptClient(req.HTTPClient) 77 | defer gock.RestoreClient(req.HTTPClient) 78 | 79 | body, err := req.Get("http://example.com/list") 80 | 81 | assert.Nil(t, err) 82 | assert.NotNil(t, body) 83 | assert.Equal(t, "{\"name\":\"mock\"}\n", body) 84 | 85 | body, err = req.Get("http://example.com/list") 86 | 87 | assert.Nil(t, err) 88 | assert.NotNil(t, body) 89 | assert.Equal(t, "{\"name\":\"mock\"}\n", body) 90 | } 91 | 92 | func Test_It_doesnt_use_cache_when_disabled(t *testing.T) { 93 | defer gock.Off() 94 | Flush() 95 | 96 | gock.New("http://example.com"). 97 | Get("/list"). 98 | Times(1). 99 | Reply(200). 100 | JSON(map[string]interface{}{"name": "mock"}) 101 | 102 | req := NewReliableRequest() 103 | req.EnableCache = false 104 | gock.InterceptClient(req.HTTPClient) 105 | defer gock.RestoreClient(req.HTTPClient) 106 | 107 | body, err := req.Get("http://example.com/list") 108 | 109 | assert.Nil(t, err) 110 | assert.NotNil(t, body) 111 | assert.Equal(t, "{\"name\":\"mock\"}\n", body) 112 | 113 | body, err = req.Get("http://example.com/list") 114 | 115 | assert.NotNil(t, err) 116 | } 117 | 118 | func Test_It_uses_stale_cache_when_enabled(t *testing.T) { 119 | defer gock.Off() 120 | Flush() 121 | 122 | gock.New("http://example.com"). 123 | Get("/list"). 124 | Times(1). 125 | Reply(200). 126 | JSON(map[string]interface{}{"name": "mock"}) 127 | 128 | req := NewReliableRequest() 129 | req.TTLCache = 1 * time.Second 130 | gock.InterceptClient(req.HTTPClient) 131 | defer gock.RestoreClient(req.HTTPClient) 132 | 133 | body, err := req.Get("http://example.com/list") 134 | 135 | assert.Nil(t, err) 136 | assert.NotNil(t, body) 137 | assert.Equal(t, "{\"name\":\"mock\"}\n", body) 138 | 139 | // simulating a cache eviction 140 | time.Sleep(2 * time.Second) 141 | 142 | body, err = req.Get("http://example.com/list") 143 | 144 | assert.Nil(t, err) 145 | } 146 | 147 | func Test_It_doesnt_use_stale_cache_when_disabled(t *testing.T) { 148 | defer gock.Off() 149 | Flush() 150 | 151 | gock.New("http://example.com"). 152 | Get("/list"). 153 | Times(1). 154 | Reply(200). 155 | JSON(map[string]interface{}{"name": "mock"}) 156 | 157 | req := NewReliableRequest() 158 | req.TTLCache = 1 * time.Second 159 | req.EnableStaleCache = false 160 | gock.InterceptClient(req.HTTPClient) 161 | defer gock.RestoreClient(req.HTTPClient) 162 | 163 | body, err := req.Get("http://example.com/list") 164 | 165 | assert.Nil(t, err) 166 | assert.NotNil(t, body) 167 | assert.Equal(t, "{\"name\":\"mock\"}\n", body) 168 | 169 | // simulating a cache eviction 170 | time.Sleep(2 * time.Second) 171 | 172 | body, err = req.Get("http://example.com/list") 173 | 174 | assert.NotNil(t, err) 175 | } 176 | 177 | func Test_It_allows_custom_headers(t *testing.T) { 178 | defer gock.Off() 179 | Flush() 180 | 181 | gock.New("http://example.com"). 182 | MatchHeader("Authorization", "^foo bar$"). 183 | Get("/list"). 184 | Times(1). 185 | Reply(200). 186 | JSON(map[string]interface{}{"name": "mock"}) 187 | 188 | req := NewReliableRequest() 189 | req.Headers = map[string]string{"Authorization": "foo bar"} 190 | 191 | gock.InterceptClient(req.HTTPClient) 192 | defer gock.RestoreClient(req.HTTPClient) 193 | 194 | body, err := req.Get("http://example.com/list") 195 | 196 | assert.Nil(t, err) 197 | assert.NotNil(t, body) 198 | assert.Equal(t, "{\"name\":\"mock\"}\n", body) 199 | } 200 | func Test_It_opens_the_circuit_breaker_when_error_percentage_is_reached(t *testing.T) { 201 | defer gock.Off() 202 | Flush() 203 | 204 | gock.New("http://example.com"). 205 | Get("/list"). 206 | Times(5). 207 | Reply(503) 208 | 209 | req := NewReliableRequest() 210 | req.UpdateHystrixConfig("custom_cb", hystrix.CommandConfig{ 211 | Timeout: 800 + 100, // the defaultTimeout http client + a small gap 212 | MaxConcurrentRequests: 100, 213 | ErrorPercentThreshold: 50, 214 | RequestVolumeThreshold: 4, 215 | SleepWindow: 5000, 216 | }) 217 | 218 | gock.InterceptClient(req.HTTPClient) 219 | defer gock.RestoreClient(req.HTTPClient) 220 | 221 | body, _ := req.Get("http://example.com/list") 222 | body, _ = req.Get("http://example.com/list") 223 | body, _ = req.Get("http://example.com/list") 224 | body, _ = req.Get("http://example.com/list") 225 | body, _ = req.Get("http://example.com/list") 226 | 227 | cb, _, _ := hystrix.GetCircuit("custom_cb") 228 | 229 | assert.Equal(t, "", body) 230 | assert.Equal(t, true, cb.IsOpen()) 231 | } 232 | func Test_It_closes_the_circuit_breaker_after_the_sleep_window(t *testing.T) { 233 | defer gock.Off() 234 | Flush() 235 | 236 | gock.New("http://example.com"). 237 | Get("/list0"). 238 | Reply(503) 239 | 240 | gock.New("http://example.com"). 241 | Get("/list1"). 242 | Reply(503) 243 | 244 | gock.New("http://example.com"). 245 | Get("/list2"). 246 | Reply(503) 247 | 248 | gock.New("http://example.com"). 249 | Get("/list3"). 250 | Reply(200). 251 | JSON(map[string]interface{}{"name": "mock"}) 252 | 253 | gock.New("http://example.com"). 254 | Get("/list4"). 255 | Reply(200). 256 | JSON(map[string]interface{}{"name": "mock"}) 257 | 258 | gock.New("http://example.com"). 259 | Get("/list5"). 260 | Reply(200). 261 | JSON(map[string]interface{}{"name": "mock"}) 262 | 263 | req := NewReliableRequest() 264 | req.UpdateHystrixConfig("custom_cb", hystrix.CommandConfig{ 265 | Timeout: 800 + 100, // the defaultTimeout http client + a small gap 266 | MaxConcurrentRequests: 100, 267 | ErrorPercentThreshold: 50, 268 | RequestVolumeThreshold: 2, 269 | SleepWindow: 2000, 270 | }) 271 | 272 | gock.InterceptClient(req.HTTPClient) 273 | defer gock.RestoreClient(req.HTTPClient) 274 | 275 | cb, _, _ := hystrix.GetCircuit("custom_cb") 276 | 277 | body, _ := req.Get("http://example.com/list0") //error 100% 278 | body, _ = req.Get("http://example.com/list1") //error 100% 279 | 280 | assert.Equal(t, "", body) 281 | assert.Equal(t, true, cb.IsOpen()) // open due to error percentage + request volume threshold 282 | 283 | body, _ = req.Get("http://example.com/list2") //error 100% 284 | body, _ = req.Get("http://example.com/list3") //error 75% 285 | 286 | time.Sleep(3500 * time.Millisecond) // simulating its sleep window 287 | 288 | body, _ = req.Get("http://example.com/list4") //error 60% 289 | body, _ = req.Get("http://example.com/list5") //error 50% 290 | 291 | assert.Equal(t, false, cb.IsOpen()) // closed due to error percentage 292 | } 293 | --------------------------------------------------------------------------------