Skip to content

Commit

Permalink
API-7853: add support for retry
Browse files Browse the repository at this point in the history
add support for retry strategy

add unit tests for retry strategy

remove debug logs, add support for basic tokens, add docs for retry
  • Loading branch information
bkubiak committed Dec 17, 2020
1 parent fee9656 commit 4e9f38f
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 23 deletions.
1 change: 1 addition & 0 deletions agent/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type agentAPI interface {
UploadFile(string, []byte) (string, error)
SetCustomHost(string)
SetCustomHeader(string, string)
SetRetryStrategy(i.RetryStrategyFunc)
}

// API provides the API operation methods for making requests to Agent Chat API via Web API.
Expand Down
85 changes: 85 additions & 0 deletions agent/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,33 @@ func createMockedErrorResponder(t *testing.T, method string) func(req *http.Requ
}
}

func createMockedMultipleAuthErrorsResponder(t *testing.T, fails int) func(req *http.Request) *http.Response {
var n int

responseError := `{
"error": {
"type": "authentication",
"message": "Invalid access token"
}
}`

return func(req *http.Request) *http.Response {
n++
if n > fails {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)),
Header: make(http.Header),
}
}
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: ioutil.NopCloser(bytes.NewBufferString(responseError)),
Header: make(http.Header),
}
}
}

func verifyErrorResponse(method string, resp error, t *testing.T) {
if resp == nil {
t.Errorf("%v should fail", method)
Expand Down Expand Up @@ -1487,3 +1514,61 @@ func TestInvalidAuthorizationScheme(t *testing.T) {
t.Errorf("Err should not be nil")
}
}

func TestRetryStrategyAllFails(t *testing.T) {
client := NewTestClient(createMockedMultipleAuthErrorsResponder(t, 10))

api, err := agent.NewAPI(stubTokenGetter(authorization.BearerToken), client, "client_id")
if err != nil {
t.Errorf("API creation failed")
}

var retries uint
api.SetRetryStrategy(func(attempts uint, err error) bool {
if attempts < 3 {
retries++
return true
}

return false
})

err = api.Call("", nil, nil)
if err == nil {
t.Errorf("Err should not be nil")
}

if retries != 3 {
t.Errorf("Retries should be done 3 times")
}

}

func TestRetryStrategyLastSuccess(t *testing.T) {
client := NewTestClient(createMockedMultipleAuthErrorsResponder(t, 2))

api, err := agent.NewAPI(stubTokenGetter(authorization.BearerToken), client, "client_id")
if err != nil {
t.Errorf("API creation failed")
}

var retries uint
api.SetRetryStrategy(func(attempts uint, err error) bool {
if attempts < 3 {
retries++
return true
}

return false
})

err = api.Call("", nil, &struct{}{})
if err != nil {
t.Errorf("Err should be nil after 2 retries")
}

if retries != 2 {
t.Errorf("Retries should be done 2 times")
}

}
1 change: 1 addition & 0 deletions configuration/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
type configurationAPI interface {
Call(string, interface{}, interface{}) error
SetCustomHost(string)
SetRetryStrategy(i.RetryStrategyFunc)
}

// API provides the API operation methods for making requests to Livechat Configuration API via Web API.
Expand Down
1 change: 1 addition & 0 deletions customer/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type customerAPI interface {
Call(string, interface{}, interface{}) error
UploadFile(string, []byte) (string, error)
SetCustomHost(string)
SetRetryStrategy(i.RetryStrategyFunc)
}

// API provides the API operation methods for making requests to Customer Chat API via Web API.
Expand Down
88 changes: 65 additions & 23 deletions internal/web_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@ import (

const apiVersion = "3.2"

// RetryStrategyFunc is called by each API method if set to retry when handling an error.
// If not set, there will be no retry at all.
//
// It accepts two arguments: attempts - number of sent requests (starting from 0)
// and err - error as ErrAPI struct (with StatusCode and Details)
// It returns info whether to retry the request.
type RetryStrategyFunc func(attempts uint, err error) bool

type api struct {
httpClient *http.Client
clientID string
tokenGetter authorization.TokenGetter
httpRequestGenerator HTTPRequestGenerator
host string
customHeaders http.Header
retryStrategy RetryStrategyFunc
}

// HTTPRequestGenerator is called by each API method to generate api http url.
Expand Down Expand Up @@ -59,12 +68,9 @@ func (a *api) Call(action string, reqPayload interface{}, respPayload interface{
if err != nil {
return err
}
token := a.tokenGetter()
if token == nil {
return fmt.Errorf("couldn't get token")
}
if token.Type != authorization.BearerToken && token.Type != authorization.BasicToken {
return fmt.Errorf("unsupported token type")
token, err := a.getToken()
if err != nil {
return err
}

req, err := a.httpRequestGenerator(token, a.host, action)
Expand Down Expand Up @@ -93,6 +99,11 @@ func (a *api) SetCustomHeader(key, val string) {
a.customHeaders.Set(key, val)
}

// SetRetryStrategy allows to set a retry strategy that will be performed in every failed request
func (a *api) SetRetryStrategy(f RetryStrategyFunc) {
a.retryStrategy = f
}

type fileUploadAPI struct{ *api }

// NewAPIWithFileUpload returns ready to use raw API client with file upload functionality.
Expand Down Expand Up @@ -133,7 +144,7 @@ func (a *fileUploadAPI) UploadFile(filename string, file []byte) (string, error)
req.Body = ioutil.NopCloser(body)

req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type, token.AccessToken))
req.Header.Set("User-agent", fmt.Sprintf("GO SDK Application %s", a.clientID))
req.Header.Set("X-Region", token.Region)

Expand All @@ -145,28 +156,59 @@ func (a *fileUploadAPI) UploadFile(filename string, file []byte) (string, error)
}

func (a *api) send(req *http.Request, respPayload interface{}) error {
resp, err := a.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
apiErr := &api_errors.ErrAPI{}
if err := json.Unmarshal(bodyBytes, apiErr); err != nil {
return fmt.Errorf("couldn't unmarshal error response: %s (code: %d, raw body: %s)", err.Error(), resp.StatusCode, string(bodyBytes))
var attempts uint
var do func() error

do = func() error {
resp, err := a.httpClient.Do(req)
if err != nil {
return err
}
if apiErr.Error() == "" {
return fmt.Errorf("couldn't unmarshal error response (code: %d, raw body: %s)", resp.StatusCode, string(bodyBytes))
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
apiErr := &api_errors.ErrAPI{}
if err := json.Unmarshal(bodyBytes, apiErr); err != nil {
return fmt.Errorf("couldn't unmarshal error response: %s (code: %d, raw body: %s)", err.Error(), resp.StatusCode, string(bodyBytes))
}
if apiErr.Error() == "" {
return fmt.Errorf("couldn't unmarshal error response (code: %d, raw body: %s)", resp.StatusCode, string(bodyBytes))
}

if a.retryStrategy == nil || !a.retryStrategy(attempts, apiErr) {
return apiErr
}

token, err := a.getToken()
if err != nil {
return err
}

req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type, token.AccessToken))
attempts++
return do()
}

if err != nil {
return err
}
return apiErr

return json.Unmarshal(bodyBytes, respPayload)
}

if err != nil {
return err
return do()
}

func (a *api) getToken() (*authorization.Token, error) {
token := a.tokenGetter()
if token == nil {
return nil, fmt.Errorf("couldn't get token")
}
if token.Type != authorization.BearerToken && token.Type != authorization.BasicToken {
return nil, fmt.Errorf("unsupported token type")
}

return json.Unmarshal(bodyBytes, respPayload)
return token, nil
}

// SetCustomHost allows to change API host address. This method is mostly for LiveChat internal testing and should not be used in production environments.
Expand Down

0 comments on commit 4e9f38f

Please sign in to comment.