Skip to content

Commit

Permalink
Merge 31c1ef9 into ef69e0b
Browse files Browse the repository at this point in the history
  • Loading branch information
sohamkamani committed Mar 27, 2019
2 parents ef69e0b + 31c1ef9 commit 90f35b7
Show file tree
Hide file tree
Showing 14 changed files with 429 additions and 13 deletions.
44 changes: 40 additions & 4 deletions README.md
Expand Up @@ -11,10 +11,13 @@
* [Description](#description)
* [Installation](#installation)
* [Usage](#usage)
+ [Making a simple `GET` request](#making-a-simple-get-request)
+ [Creating a hystrix-like circuit breaker](#creating-a-hystrix-like-circuit-breaker)
+ [Creating an HTTP client with a retry mechanism](#creating-an-http-client-with-a-retry-mechanism)
+ [Custom retry mechanisms](#custom-retry-mechanisms)
+ [Making a simple `GET` request](#making-a-simple-get-request)
+ [Creating a hystrix-like circuit breaker](#creating-a-hystrix-like-circuit-breaker)
+ [Creating a hystrix-like circuit breaker with fallbacks](#creating-a-hystrix-like-circuit-breaker-with-fallbacks)
+ [Creating an HTTP client with a retry mechanism](#creating-an-http-client-with-a-retry-mechanism)
+ [Custom retry mechanisms](#custom-retry-mechanisms)
+ [Custom HTTP clients](#custom-http-clients)
* [Plugins](#plugins)
* [Documentation](#documentation)
* [FAQ](#faq)
* [License](#license)
Expand Down Expand Up @@ -294,6 +297,39 @@ client := httpclient.NewClient(
// The rest is the same as the first example
```

## Plugins

To add a plugin to an existing client, use the `AddPlugin` method of the client.

An example, with the [request logger plugin](/plugins/request_logger.go):

```go
// import "github.com/gojektech/heimdall/plugins"

client := heimdall.NewHTTPClient(timeout)
requestLogger := plugins.NewRequestLogger(nil, nil)
client.AddPlugin(requestLogger)
// use the client as before

req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
res, err := client.Do(req)
if err != nil {
panic(err)
}
// This will log:
//23/Jun/2018 12:48:04 GET http://google.com 200 [412ms]
// to STDOUT
```

A plugin is an interface whose methods get called during key events in a requests lifecycle:

- `OnRequestStart` is called just before the request is made
- `OnRequestEnd` is called once the request has successfully executed
- `OnError` is called is the request failed

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.
For a simple example on how to write plugins, look at the [request logger plugin](/plugins/request_logger.go).

## Documentation

Further documentation can be found on [godoc.org](https://www.godoc.org/github.com/gojektech/heimdall)
Expand Down
1 change: 1 addition & 0 deletions client.go
Expand Up @@ -19,4 +19,5 @@ type Client interface {
Patch(url string, body io.Reader, headers http.Header) (*http.Response, error)
Delete(url string, headers http.Header) (*http.Response, error)
Do(req *http.Request) (*http.Response, error)
AddPlugin(p Plugin)
}
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -10,5 +10,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.1
)
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -16,5 +16,7 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
28 changes: 27 additions & 1 deletion httpclient/client.go
Expand Up @@ -19,6 +19,7 @@ type Client struct {
timeout time.Duration
retryCount int
retrier heimdall.Retriable
plugins []heimdall.Plugin
}

const (
Expand Down Expand Up @@ -49,6 +50,11 @@ func NewClient(opts ...Option) *Client {
return &client
}

// AddPlugin Adds plugin to client
func (c *Client) AddPlugin(p heimdall.Plugin) {
c.plugins = append(c.plugins, p)
}

// Get makes a HTTP GET request to provided URL
func (c *Client) Get(url string, headers http.Header) (*http.Response, error) {
var response *http.Response
Expand Down Expand Up @@ -137,6 +143,7 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
response.Body.Close()
}

c.reportRequestStart(request)
var err error
response, err = c.client.Do(request)
if bodyReader != nil {
Expand All @@ -147,11 +154,12 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {

if err != nil {
multiErr.Push(err.Error())

c.reportError(request, err)
backoffTime := c.retrier.NextInterval(i)
time.Sleep(backoffTime)
continue
}
c.reportRequestEnd(request, response)

if response.StatusCode >= http.StatusInternalServerError {
backoffTime := c.retrier.NextInterval(i)
Expand All @@ -165,3 +173,21 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {

return response, multiErr.HasError()
}

func (c *Client) reportRequestStart(request *http.Request) {
for _, plugin := range c.plugins {
plugin.OnRequestStart(request)
}
}

func (c *Client) reportError(request *http.Request, err error) {
for _, plugin := range c.plugins {
plugin.OnError(request, err)
}
}

func (c *Client) reportRequestEnd(request *http.Request, response *http.Response) {
for _, plugin := range c.plugins {
plugin.OnRequestEnd(request, response)
}
}
55 changes: 55 additions & 0 deletions httpclient/client_test.go
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/gojektech/heimdall"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -417,6 +418,60 @@ func TestHTTPClientGetReturnsErrorOnFailure(t *testing.T) {

}

func TestPluginMethodsCalled(t *testing.T) {
client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
mockPlugin := &MockPlugin{}
client.AddPlugin(mockPlugin)

dummyHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{ "response": "something went wrong" }`))
}

server := httptest.NewServer(http.HandlerFunc(dummyHandler))
defer server.Close()

mockPlugin.On("OnRequestStart", mock.Anything)
mockPlugin.On("OnRequestEnd", mock.Anything, mock.Anything)

_, err := client.Get(server.URL, http.Header{})

require.NoError(t, err)
mockPlugin.AssertNumberOfCalls(t, "OnRequestStart", 1)
pluginRequest, ok := mockPlugin.Calls[0].Arguments[0].(*http.Request)
require.True(t, ok)
assert.Equal(t, http.MethodGet, pluginRequest.Method)
assert.Equal(t, server.URL, pluginRequest.URL.String())

mockPlugin.AssertNumberOfCalls(t, "OnRequestEnd", 1)
pluginResponse, ok := mockPlugin.Calls[1].Arguments[1].(*http.Response)
require.True(t, ok)
assert.Equal(t, http.StatusOK, pluginResponse.StatusCode)
}

func TestPluginErrorMethodCalled(t *testing.T) {
client := NewClient(WithHTTPTimeout(10 * time.Millisecond))
mockPlugin := &MockPlugin{}
client.AddPlugin(mockPlugin)

mockPlugin.On("OnRequestStart", mock.Anything)
mockPlugin.On("OnError", mock.Anything, mock.Anything)

serverURL := "does_not_exist"
_, err := client.Get(serverURL, http.Header{})

mockPlugin.AssertNumberOfCalls(t, "OnRequestStart", 1)
pluginRequest, ok := mockPlugin.Calls[0].Arguments[0].(*http.Request)
require.True(t, ok)
assert.Equal(t, http.MethodGet, pluginRequest.Method)
assert.Equal(t, serverURL, pluginRequest.URL.String())

mockPlugin.AssertNumberOfCalls(t, "OnError", 1)
err, ok = mockPlugin.Calls[1].Arguments[1].(error)
require.True(t, ok)
assert.EqualError(t, err, "Get does_not_exist: unsupported protocol scheme \"\"")
}

type myHTTPClient struct {
client http.Client
}
Expand Down
87 changes: 87 additions & 0 deletions httpclient/options_test.go
@@ -1,6 +1,7 @@
package httpclient

import (
"fmt"
"net/http"
"testing"
"time"
Expand Down Expand Up @@ -44,3 +45,89 @@ func TestOptionsHaveDefaults(t *testing.T) {
assert.Equal(t, retrier, c.retrier)
assert.Equal(t, noOfRetries, c.retryCount)
}

func ExampleWithHTTPTimeout() {
c := NewClient(WithHTTPTimeout(5 * time.Second))
req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
if err != nil {
panic(err)
}
res, err := c.Do(req)
if err != nil {
panic(err)
}
fmt.Println("Response status : ", res.StatusCode)
// Output: Response status : 200
}

func ExampleWithHTTPTimeout_expired() {
c := NewClient(WithHTTPTimeout(1 * time.Millisecond))
req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
if err != nil {
panic(err)
}
res, err := c.Do(req)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("Response status : ", res.StatusCode)
}

func ExampleWithRetryCount() {
c := NewClient(WithHTTPTimeout(1*time.Millisecond), WithRetryCount(3))
req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
if err != nil {
panic(err)
}
res, err := c.Do(req)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("Response status : ", res.StatusCode)
}

type mockClient struct{}

func (m *mockClient) Do(r *http.Request) (*http.Response, error) {
fmt.Println("mock client called")
return &http.Response{}, nil
}

func ExampleWithHTTPClient() {
m := &mockClient{}
c := NewClient(WithHTTPClient(m))
req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
if err != nil {
panic(err)
}
_, _ = c.Do(req)
// Output: mock client called
}

type mockRetrier struct{}

func (m *mockRetrier) NextInterval(attempt int) time.Duration {
fmt.Println("retry attempt", attempt)
return time.Millisecond
}

func ExampleWithRetrier() {
c := NewClient(WithHTTPTimeout(1*time.Millisecond), WithRetryCount(3), WithRetrier(&mockRetrier{}))
req, err := http.NewRequest(http.MethodGet, "https://www.gojek.io/", nil)
if err != nil {
panic(err)
}
res, err := c.Do(req)
if err != nil {
fmt.Println("error")
return
}
fmt.Println("Response status : ", res.StatusCode)
// Output: retry attempt 0
// retry attempt 1
// retry attempt 2
// retry attempt 3
// error
}
27 changes: 27 additions & 0 deletions httpclient/plugin_mock.go
@@ -0,0 +1,27 @@
package httpclient

import (
"net/http"

"github.com/stretchr/testify/mock"
)

// MockPlugin provides a mock plugin for heimdall
type MockPlugin struct {
mock.Mock
}

// OnRequestStart is called when the request starts
func (m *MockPlugin) OnRequestStart(req *http.Request) {
m.Called(req)
}

// OnRequestEnd is called when the request ends
func (m *MockPlugin) OnRequestEnd(req *http.Request, res *http.Response) {
m.Called(req, res)
}

// OnError is called when the request errors out
func (m *MockPlugin) OnError(req *http.Request, err error) {
m.Called(req, err)
}
15 changes: 8 additions & 7 deletions hystrix/hystrix_client.go
Expand Up @@ -2,6 +2,7 @@ package hystrix

import (
"bytes"
"github.com/gojektech/heimdall/httpclient"
"io"
"io/ioutil"
"net/http"
Expand All @@ -16,7 +17,7 @@ type fallbackFunc func(error) error

// Client is the hystrix client implementation
type Client struct {
client heimdall.Doer
client *httpclient.Client

timeout time.Duration
hystrixTimeout time.Duration
Expand Down Expand Up @@ -49,6 +50,7 @@ var err5xx = errors.New("server returned 5xx status code")
// NewClient returns a new instance of hystrix Client
func NewClient(opts ...Option) *Client {
client := Client{
client: httpclient.NewClient(),
timeout: defaultHTTPTimeout,
hystrixTimeout: defaultHystrixTimeout,
maxConcurrentRequests: defaultMaxConcurrentRequests,
Expand All @@ -63,12 +65,6 @@ func NewClient(opts ...Option) *Client {
opt(&client)
}

if client.client == nil {
client.client = &http.Client{
Timeout: client.timeout,
}
}

hystrix.ConfigureCommand(client.hystrixCommandName, hystrix.CommandConfig{
Timeout: durationToInt(client.hystrixTimeout, time.Millisecond),
MaxConcurrentRequests: client.maxConcurrentRequests,
Expand Down Expand Up @@ -211,3 +207,8 @@ func (hhc *Client) Do(request *http.Request) (*http.Response, error) {

return response, err
}

// AddPlugin Adds plugin to client
func (hhc *Client) AddPlugin(p heimdall.Plugin) {
hhc.client.AddPlugin(p)
}

0 comments on commit 90f35b7

Please sign in to comment.