Skip to content

Commit 733c2b5

Browse files
committedNov 14, 2015
Retry only when HTTP request fails
`PerformRequest` retried even on valid responses, e.g. when ES returned a 404. However, 404 can be a valid response from Elasticsearch for certain services, e.g. the Exists service. Now we only retry when `HttpRequest.Do` returns an error. This commit also refactors error handling for services that return a 404 as normal behavior, e.g. `ExistsService`, `IndicesExistsService` and `IndicesExistsTypeService`. The error code is now handled in the service, not in the generic error handler (`errors.go`). Instead, one can pass HTTP error codes to ignore now to the error handler and to `PerformRequest`.
1 parent 91197e2 commit 733c2b5

7 files changed

+80
-31
lines changed
 

‎client.go

+9-11
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222

2323
const (
2424
// Version is the current version of Elastic.
25-
Version = "3.0.3"
25+
Version = "3.0.4"
2626

2727
// DefaultUrl is the default endpoint of Elasticsearch on the local machine.
2828
// It is used e.g. when initializing a new Client without a specific URL.
@@ -900,7 +900,11 @@ func (c *Client) mustActiveConn() error {
900900

901901
// PerformRequest does a HTTP request to Elasticsearch.
902902
// It returns a response and an error on failure.
903-
func (c *Client) PerformRequest(method, path string, params url.Values, body interface{}) (*Response, error) {
903+
//
904+
// Optionally, a list of HTTP error codes to ignore can be passed.
905+
// This is necessary for services that expect e.g. HTTP status 404 as a
906+
// valid outcome (Exists, IndicesExists, IndicesTypeExists).
907+
func (c *Client) PerformRequest(method, path string, params url.Values, body interface{}, ignoreErrors ...int) (*Response, error) {
904908
start := time.Now().UTC()
905909

906910
c.mu.RLock()
@@ -992,15 +996,9 @@ func (c *Client) PerformRequest(method, path string, params url.Values, body int
992996
}
993997

994998
// Check for errors
995-
if err := checkResponse((*http.Request)(req), res); err != nil {
996-
retries -= 1
997-
if retries <= 0 {
998-
return nil, err
999-
}
1000-
retried = true
1001-
time.Sleep(time.Duration(retryWaitMsec) * time.Millisecond)
1002-
retryWaitMsec += retryWaitMsec
1003-
continue // try again
999+
if err := checkResponse((*http.Request)(req), res, ignoreErrors...); err != nil {
1000+
// No retry if request succeeded
1001+
return nil, err
10041002
}
10051003

10061004
// Tracing

‎client_test.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package elastic
77
import (
88
"bytes"
99
"encoding/json"
10+
"errors"
1011
"log"
1112
"net/http"
1213
"regexp"
@@ -664,11 +665,12 @@ func (tr *failingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
664665
return http.DefaultTransport.RoundTrip(r)
665666
}
666667

667-
func TestPerformRequestWithMaxRetries(t *testing.T) {
668+
func TestPerformRequestRetryOnHttpError(t *testing.T) {
668669
var numFailedReqs int
669670
fail := func(r *http.Request) (*http.Response, error) {
670671
numFailedReqs += 1
671-
return &http.Response{Request: r, StatusCode: 400}, nil
672+
//return &http.Response{Request: r, StatusCode: 400}, nil
673+
return nil, errors.New("request failed")
672674
}
673675

674676
// Run against a failing endpoint and see if PerformRequest
@@ -693,3 +695,33 @@ func TestPerformRequestWithMaxRetries(t *testing.T) {
693695
t.Errorf("expected %d failed requests; got: %d", 5, numFailedReqs)
694696
}
695697
}
698+
699+
func TestPerformRequestNoRetryOnValidButUnsuccessfulHttpStatus(t *testing.T) {
700+
var numFailedReqs int
701+
fail := func(r *http.Request) (*http.Response, error) {
702+
numFailedReqs += 1
703+
return &http.Response{Request: r, StatusCode: 500}, nil
704+
}
705+
706+
// Run against a failing endpoint and see if PerformRequest
707+
// retries correctly.
708+
tr := &failingTransport{path: "/fail", fail: fail}
709+
httpClient := &http.Client{Transport: tr}
710+
711+
client, err := NewClient(SetHttpClient(httpClient), SetMaxRetries(5))
712+
if err != nil {
713+
t.Fatal(err)
714+
}
715+
716+
res, err := client.PerformRequest("GET", "/fail", nil, nil)
717+
if err == nil {
718+
t.Fatal("expected error")
719+
}
720+
if res != nil {
721+
t.Fatal("expected no response")
722+
}
723+
// Retry should not have triggered additional requests because
724+
if numFailedReqs != 1 {
725+
t.Errorf("expected %d failed requests; got: %d", 1, numFailedReqs)
726+
}
727+
}

‎errors.go

+17-8
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,38 @@ var (
2121
// an error returned from Elasticsearch.
2222
//
2323
// HTTP status codes between in the range [200..299] are considered successful.
24-
// Also, HTTP requests with method HEAD that return 404 are considered
25-
// to be no errors. All other request/response combinations return an error.
24+
// All other errors are considered errors except they are specified in
25+
// ignoreErrors. This is necessary because for some services, HTTP status 404
26+
// is a valid response from Elasticsearch (e.g. the Exists service).
2627
//
2728
// The func tries to parse error details as returned from Elasticsearch
2829
// and encapsulates them in type elastic.Error.
29-
func checkResponse(req *http.Request, res *http.Response) error {
30+
func checkResponse(req *http.Request, res *http.Response, ignoreErrors ...int) error {
3031
// 200-299 are valid status codes
3132
if res.StatusCode >= 200 && res.StatusCode <= 299 {
3233
return nil
3334
}
34-
// HEAD requests with 404 are not an error
35-
if req.Method == "HEAD" && res.StatusCode == 404 {
36-
return nil
35+
// Ignore certain errors?
36+
for _, code := range ignoreErrors {
37+
if code == res.StatusCode {
38+
return nil
39+
}
3740
}
41+
return createResponseError(res)
42+
}
43+
44+
// createResponseError creates an Error structure from the HTTP response,
45+
// its status code and the error information sent by Elasticsearch.
46+
func createResponseError(res *http.Response) error {
3847
if res.Body == nil {
3948
return &Error{Status: res.StatusCode}
4049
}
41-
slurp, err := ioutil.ReadAll(res.Body)
50+
data, err := ioutil.ReadAll(res.Body)
4251
if err != nil {
4352
return &Error{Status: res.StatusCode}
4453
}
4554
errReply := new(Error)
46-
err = json.Unmarshal(slurp, errReply)
55+
err = json.Unmarshal(data, errReply)
4756
if err != nil {
4857
return &Error{Status: res.StatusCode}
4958
}

‎errors_test.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestResponseErrorHTML(t *testing.T) {
104104
}
105105
}
106106

107-
func TestResponseErrorWithHeadMethodAndNotFound(t *testing.T) {
107+
func TestResponseErrorWithIgnore(t *testing.T) {
108108
raw := "HTTP/1.1 404 Not Found\r\n" +
109109
"\r\n" +
110110
`{"some":"response"}` + "\r\n"
@@ -120,6 +120,10 @@ func TestResponseErrorWithHeadMethodAndNotFound(t *testing.T) {
120120
t.Fatal(err)
121121
}
122122
err = checkResponse(req, resp)
123+
if err == nil {
124+
t.Fatalf("expected error; got: %v", err)
125+
}
126+
err = checkResponse(req, resp, 404) // ignore 404 errors
123127
if err != nil {
124128
t.Fatalf("expected no error; got: %v", err)
125129
}

‎exists.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func (s *ExistsService) Do() (bool, error) {
158158
}
159159

160160
// Get HTTP response
161-
res, err := s.client.PerformRequest("HEAD", path, params, nil)
161+
res, err := s.client.PerformRequest("HEAD", path, params, nil, 404)
162162
if err != nil {
163163
return false, err
164164
}

‎indices_exists.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package elastic
66

77
import (
88
"fmt"
9+
"net/http"
910
"net/url"
1011
"strings"
1112

@@ -131,16 +132,18 @@ func (s *IndicesExistsService) Do() (bool, error) {
131132
}
132133

133134
// Get HTTP response
134-
res, err := s.client.PerformRequest("HEAD", path, params, nil)
135+
res, err := s.client.PerformRequest("HEAD", path, params, nil, 404)
135136
if err != nil {
136137
return false, err
137138
}
138139

139140
// Return operation response
140-
if res.StatusCode == 200 {
141+
switch res.StatusCode {
142+
case http.StatusOK:
141143
return true, nil
142-
} else if res.StatusCode == 404 {
144+
case http.StatusNotFound:
143145
return false, nil
146+
default:
147+
return false, fmt.Errorf("elastic: got HTTP code %d when it should have been either 200 or 404", res.StatusCode)
144148
}
145-
return false, fmt.Errorf("elastic: got HTTP code %d when it should have been either 200 or 404", res.StatusCode)
146149
}

‎indices_exists_type.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package elastic
66

77
import (
88
"fmt"
9+
"net/http"
910
"net/url"
1011
"strings"
1112

@@ -143,16 +144,18 @@ func (s *IndicesExistsTypeService) Do() (bool, error) {
143144
}
144145

145146
// Get HTTP response
146-
res, err := s.client.PerformRequest("HEAD", path, params, nil)
147+
res, err := s.client.PerformRequest("HEAD", path, params, nil, 404)
147148
if err != nil {
148149
return false, err
149150
}
150151

151152
// Return operation response
152-
if res.StatusCode == 200 {
153+
switch res.StatusCode {
154+
case http.StatusOK:
153155
return true, nil
154-
} else if res.StatusCode == 404 {
156+
case http.StatusNotFound:
155157
return false, nil
158+
default:
159+
return false, fmt.Errorf("elastic: got HTTP code %d when it should have been either 200 or 404", res.StatusCode)
156160
}
157-
return false, fmt.Errorf("elastic: got HTTP code %d when it should have been either 200 or 404", res.StatusCode)
158161
}

0 commit comments

Comments
 (0)
Please sign in to comment.