Merge lp:~rvb/gwacl/retry-feature into lp:gwacl

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: 220
Merged at revision: 217
Proposed branch: lp:~rvb/gwacl/retry-feature
Merge into: lp:gwacl
Diff against target: 702 lines (+390/-37)
10 files modified
management_base.go (+17/-6)
management_base_test.go (+22/-5)
retry_policy.go (+122/-0)
retry_policy_test.go (+150/-0)
storage_base.go (+5/-1)
storage_base_test.go (+22/-0)
x509dispatcher.go (+7/-5)
x509dispatcher_test.go (+28/-5)
x509session.go (+3/-1)
x509session_test.go (+14/-14)
To merge this branch: bzr merge lp:~rvb/gwacl/retry-feature
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+179212@code.launchpad.net

Commit message

Add retry feature.

Description of the change

This branch adds a "retry feature" to both the management and the storage api. This feature allows a user to configure the api objects in such a way that certain response status codes (typically 409 or 50* — but this is entirely user-configurable) will cause the original request to be retried a certain number of times.

Note that the default behavior (when no retry policy is specified) is unchanged: no retry will occur.

The fact that we have two completely different API objects for the storage side and the management side make this change a bit repetitive but all the logic is encapsulated in the retry policy objects.

This is all really optional so I chose adding a new constructor method (NewManagementAPIWithRetryPolicy) on the management side rather than forcing the user to specify a retry policy when calling NewManagementAPI().

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Just starting to review this. Your commit message is terrible :) I'd copy/paste the description into it instead quite frankly, it's much better.

Revision history for this message
Julian Edwards (julian-edwards) wrote :
Download full text (4.1 KiB)

Looks good and is pretty well tested, although I think the "retrier" is not really a retrier!
Please consider my suggestion about it below, although it might be a larger change than you want
to do right now. Otherwise there's a few small things you can change and then land away.

General observation - where you're using time.Millisecond in your tests for the retry
interval, please use time.Nanosecond. No need to keep the tests waiting any longer :)

117 +// It it usually created using RetryPolicy.getRetrier() and then used by

Typo "It it" -> "it is"

117 +// It it usually created using RetryPolicy.getRetrier() and then used by
118 +// performing the request governed by the policy the retrier enforces
119 +// repeatedly until retrier.shouldRetry() returns false.

This sentence is pretty hard to read, I've scanned it several times before working it out.
Let me suggest an alternative:

// retrier is usually created using RetryPolicy.getRetrier(). The retrier enforces the
// policy during repeated requests until retrier.shouldRetry() returns false.

137 + ret.retriesLeft -= 1

Go has ++ and --, might as well use -- here:
    ret.retriesLeft--

I'm not sure it's a good idea to mix the sleep inside this function, it would be better
if it stuck to the single task of making the decision of whether to retry or not and
allowing the callsite to sleep if it wants. As it stands, it's a strange mix of functionality.

It might be an idea to return a time.Delay instead of a bool, which tells the caller how
long to sleep, where zero means "don't retry". You could then fold that into another
receiver method on retrier which will do all of check, wait and retry.

We could define an interface that specs the function out like this so that we can use it in
the storage and the management side:

type RetryableRequest interface {
    func RetryRequest(http.Request) (http.Response, error)
}

and then all the retrying is *completely* encapsulated in the, well, retrier :)

171 +func (*retryPolicySuite) TestIsRetryCodeChecksStatusCode(c *C) {
172 + var testValues = []struct {
173 + retryStatusCodes []int
174 + testStatusCode int
175 + expectedOutcome bool
176 + }{
177 + {[]int{http.StatusConflict, http.StatusInternalServerError}, http.StatusConflict, true},
178 + {[]int{http.StatusConflict}, http.StatusConflict, true},
179 + {[]int{http.StatusConflict}, http.StatusOK, false},
180 + {[]int{}, http.StatusConflict, false},
181 + }
182 + for _, test := range testValues {
183 + policy := RetryPolicy{httpStatusCodes: test.retryStatusCodes}
184 + c.Check(policy.isRetryCode(test.testStatusCode), Equals, test.expectedOutcome)
185 + }

You have a habit of doing these looping tests! I'm not sure this is any more readable than four
simple calls to c.Check. In fact I'm pretty sure it isn't!

In fact there's an argument to break it into four separate tests, whose names tell me exactly what
scenario is being tested.

248 + for {
...
259 + // Check if the context's retry policy commands to retry the request.
260 + if !retrier.shouldRetry(resp.StatusCode) {
261 + bre...

Read more...

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

> type RetryableRequest interface {
> func RetryRequest(http.Request) (http.Response, error)
> }
>

I think this is a great idea. It will be a bit more complicated than what you think because encapsulating the part where the request is being performed means encapsulating a 'http.client' object and we have two different client (the one from the std lib and the forked client) but with another interface, I think this can be done.

lp:~rvb/gwacl/retry-feature updated
219. By Raphaël Badin

Move retry in utility method.

Revision history for this message
Raphaël Badin (rvb) wrote :
Download full text (3.9 KiB)

Thanks for the review.

> General observation - where you're using time.Millisecond in your tests for
> the retry
> interval, please use time.Nanosecond. No need to keep the tests waiting any
> longer :)

All right :).

> 117 +// It it usually created using RetryPolicy.getRetrier() and then used
> by
>
> Typo "It it" -> "it is"

Fixed.

> 117 +// It it usually created using RetryPolicy.getRetrier() and then used
> by
> 118 +// performing the request governed by the policy the retrier enforces
> 119 +// repeatedly until retrier.shouldRetry() returns false.
>
> This sentence is pretty hard to read, I've scanned it several times before
> working it out.
> Let me suggest an alternative:
>
> // retrier is usually created using RetryPolicy.getRetrier(). The retrier
> enforces the
> // policy during repeated requests until retrier.shouldRetry() returns false.

I've changed the comment.

> 137 + ret.retriesLeft -= 1
>
> Go has ++ and --, might as well use -- here:
> ret.retriesLeft--

Done.

> I'm not sure it's a good idea to mix the sleep inside this function, it would
> be better
> if it stuck to the single task of making the decision of whether to retry or
> not and
> allowing the callsite to sleep if it wants. As it stands, it's a strange mix
> of functionality.
>
> It might be an idea to return a time.Delay instead of a bool, which tells the
> caller how
> long to sleep, where zero means "don't retry". You could then fold that into
> another
> receiver method on retrier which will do all of check, wait and retry.
>
> We could define an interface that specs the function out like this so that we
> can use it in
> the storage and the management side:
>
> type RetryableRequest interface {
> func RetryRequest(http.Request) (http.Response, error)
> }
>
> and then all the retrying is *completely* encapsulated in the, well, retrier
> :)
>
> 171 +func (*retryPolicySuite) TestIsRetryCodeChecksStatusCode(c *C) {
> 172 + var testValues = []struct {
> 173 + retryStatusCodes []int
> 174 + testStatusCode int
> 175 + expectedOutcome bool
> 176 + }{
> 177 + {[]int{http.StatusConflict, http.StatusInternalServerError},
> http.StatusConflict, true},
> 178 + {[]int{http.StatusConflict}, http.StatusConflict, true},
> 179 + {[]int{http.StatusConflict}, http.StatusOK, false},
> 180 + {[]int{}, http.StatusConflict, false},
> 181 + }
> 182 + for _, test := range testValues {
> 183 + policy := RetryPolicy{httpStatusCodes: test.retryStatusCodes}
> 184 + c.Check(policy.isRetryCode(test.testStatusCode), Equals,
> test.expectedOutcome)
> 185 + }
>
> You have a habit of doing these looping tests! I'm not sure this is any more
> readable than four
> simple calls to c.Check. In fact I'm pretty sure it isn't!

That's right, that construction rarely pays off in practice :/. Fixed

> Again this could be done much sooner inside a smaller loop, I'm not sure of
> the benefit of
> all the other body processing if we are simply going to retry anyway.

All right, I've refactored the code significant...

Read more...

lp:~rvb/gwacl/retry-feature updated
220. By Raphaël Badin

Export variables so this can be used from outside the library.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

I can't help feeling that we're missing an interface somewhere that would simplify this considerably, but this is already looking much better than before. I'd comment a bit more in the two retrier objects that they are split because of storage and management needing two different clients.

Land away!

Revision history for this message
Raphaël Badin (rvb) wrote :

> I can't help feeling that we're missing an interface somewhere that would
> simplify this considerably, but this is already looking much better than
> before.

Well, the problem is that the two sides don't deal with the same response/request objects. That's why I think it's not really possible to simplify this with a common interface. The only way it could work is if we were to define abstractions over the different request/response objects but the benefit-cost ratio would be rather poor I'm afraid.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'management_base.go'
--- management_base.go 2013-08-06 05:11:29 +0000
+++ management_base.go 2013-08-09 15:00:01 +0000
@@ -32,15 +32,22 @@
32// The default duration after which the polling is terminated.32// The default duration after which the polling is terminated.
33const DefaultPollerTimeout = 20 * time.Minute33const DefaultPollerTimeout = 20 * time.Minute
3434
35// NewManagementAPIWithRetryPolicy creates an object used to interact with
36// Windows Azure's API.
37// http://msdn.microsoft.com/en-us/library/windowsazure/ff800682.aspx
38func NewManagementAPIWithRetryPolicy(subscriptionId, certFile, location string, policy RetryPolicy) (*ManagementAPI, error) {
39 session, err := newX509Session(subscriptionId, certFile, location, policy)
40 if err != nil {
41 return nil, err
42 }
43 api := ManagementAPI{session, DefaultPollerInterval, DefaultPollerTimeout}
44 return &api, nil
45}
46
35// NewManagementAPI creates an object used to interact with Windows Azure's API.47// NewManagementAPI creates an object used to interact with Windows Azure's API.
36// http://msdn.microsoft.com/en-us/library/windowsazure/ff800682.aspx48// http://msdn.microsoft.com/en-us/library/windowsazure/ff800682.aspx
37func NewManagementAPI(subscriptionId, certFile, location string) (*ManagementAPI, error) {49func NewManagementAPI(subscriptionId, certFile, location string) (*ManagementAPI, error) {
38 session, err := newX509Session(subscriptionId, certFile, location)50 return NewManagementAPIWithRetryPolicy(subscriptionId, certFile, location, NoRetryPolicy)
39 if err != nil {
40 return nil, err
41 }
42 api := ManagementAPI{session, DefaultPollerInterval, DefaultPollerTimeout}
43 return &api, nil
44}51}
4552
46var operationIDHeaderName = http.CanonicalHeaderKey("x-ms-request-id")53var operationIDHeaderName = http.CanonicalHeaderKey("x-ms-request-id")
@@ -56,6 +63,10 @@
56 return "", err63 return "", err
57}64}
5865
66func (api *ManagementAPI) GetRetryPolicy() RetryPolicy {
67 return api.session.retryPolicy
68}
69
59// blockUntilCompleted blocks and polls for completion of an Azure operation.70// blockUntilCompleted blocks and polls for completion of an Azure operation.
60// The "response" parameter is the result of the request that started the71// The "response" parameter is the result of the request that started the
61// operation. If the response says that the operation is running72// operation. If the response says that the operation is running
6273
=== modified file 'management_base_test.go'
--- management_base_test.go 2013-08-06 05:31:49 +0000
+++ management_base_test.go 2013-08-09 15:00:01 +0000
@@ -234,11 +234,28 @@
234 c.Assert(err, IsNil)234 c.Assert(err, IsNil)
235235
236 api, err := NewManagementAPI(subscriptionId, certFile, "West US")236 api, err := NewManagementAPI(subscriptionId, certFile, "West US")
237237 c.Assert(err, IsNil)
238 c.Assert(err, IsNil)238
239 session, err := newX509Session(subscriptionId, certFile, "West US")239 c.Assert(api.session.subscriptionId, DeepEquals, subscriptionId)
240 c.Assert(api.session.subscriptionId, DeepEquals, session.subscriptionId)240 c.Assert(api.session.certFile, DeepEquals, certFile)
241 c.Assert(api.session.certFile, DeepEquals, session.certFile)241 c.Assert(api.session.retryPolicy, DeepEquals, NoRetryPolicy)
242}
243
244func (suite *managementBaseAPISuite) TestNewManagementAPIWithRetryPolicy(c *C) {
245 subscriptionId := "subscriptionId"
246 certDir := c.MkDir()
247 certFile := certDir + "/cert.pem"
248 err := ioutil.WriteFile(certFile, []byte(testCert), 0600)
249 c.Assert(err, IsNil)
250 retryPolicy := RetryPolicy{NbRetries: 5, HttpStatusCodes: []int{409}, Delay: time.Minute}
251
252 api, err := NewManagementAPIWithRetryPolicy(subscriptionId, certFile, "West US", retryPolicy)
253 c.Assert(err, IsNil)
254
255 c.Assert(api.session.subscriptionId, DeepEquals, subscriptionId)
256 c.Assert(api.session.certFile, DeepEquals, certFile)
257 c.Assert(api.session.retryPolicy, DeepEquals, retryPolicy)
258 c.Assert(api.GetRetryPolicy(), DeepEquals, retryPolicy)
242}259}
243260
244func (suite *managementBaseAPISuite) TestNewManagementAPISetsDefaultPollerInterval(c *C) {261func (suite *managementBaseAPISuite) TestNewManagementAPISetsDefaultPollerInterval(c *C) {
245262
=== added file 'retry_policy.go'
--- retry_policy.go 1970-01-01 00:00:00 +0000
+++ retry_policy.go 2013-08-09 15:00:01 +0000
@@ -0,0 +1,122 @@
1// Copyright 2013 Canonical Ltd. This software is licensed under the
2// GNU Lesser General Public License version 3 (see the file COPYING).
3
4package gwacl
5
6import (
7 forkedHttp "launchpad.net/gwacl/fork/http"
8 "net/http"
9 "time"
10)
11
12// A RetryPolicy object encapsulates all the information needed to define how
13// requests should be retried when particular response codes are returned by
14// the Windows Azure server.
15type RetryPolicy struct {
16 // The number of times a request could be retried. This does not account
17 // for the initial request so a value of 3 means that the request might be
18 // performed 4 times in total.
19 NbRetries int
20 // The HTTP status codes of the response for which the request should be
21 // retried.
22 HttpStatusCodes []int
23 // How long the client should wait between retries.
24 Delay time.Duration
25}
26
27var (
28 NoRetryPolicy = RetryPolicy{NbRetries: 0}
29)
30
31// isRetryCode returns whether or not the given http status code indicates that
32// the request should be retried according to this policy.
33func (policy RetryPolicy) isRetryCode(httpStatusCode int) bool {
34 for _, code := range policy.HttpStatusCodes {
35 if code == httpStatusCode {
36 return true
37 }
38 }
39 return false
40}
41
42func (policy RetryPolicy) getRetryHelper() *retryHelper {
43 return &retryHelper{retriesLeft: policy.NbRetries, policy: &policy}
44}
45
46// A retryHelper is a utility object used to enforce a retry policy when
47// performing requests.
48type retryHelper struct {
49 // The maximum number of retries left to perform.
50 retriesLeft int
51 // The `RetryPolicy` enforced by this retrier.
52 policy *RetryPolicy
53}
54
55// shouldRetry returns whether or not a request governed by the underlying
56// retry policy should be retried. When it returns 'true', `shouldRetry` also
57// waits for the specified amount of time, as dictated by the retry policy.
58func (ret *retryHelper) shouldRetry(httpStatusCode int) bool {
59 if ret.retriesLeft > 0 && ret.policy.isRetryCode(httpStatusCode) {
60 ret.retriesLeft--
61 return true
62 }
63 return false
64}
65
66// A retrier is a struct used to repeat a request as governed by a retry
67// policy. retrier is usually created using RetryPolicy.getRetrier().
68type retrier struct {
69 *retryHelper
70
71 // The client used to perform requests.
72 client *http.Client
73}
74
75func (ret *retrier) RetryRequest(request *http.Request) (*http.Response, error) {
76 for {
77 response, err := ret.client.Do(request)
78 if err != nil {
79 return nil, err
80 }
81 if !ret.shouldRetry(response.StatusCode) {
82 return response, nil
83 }
84 time.Sleep(ret.policy.Delay)
85 }
86}
87
88// getRetrier returns a `retrier` object used to enforce the retry policy.
89func (policy RetryPolicy) getRetrier(client *http.Client) *retrier {
90 helper := policy.getRetryHelper()
91 return &retrier{retryHelper: helper, client: client}
92}
93
94// A forkedHttpRetrier is a struct used to repeat a request as governed by a
95// retry policy. forkedHttpRetrier is usually created using
96// RetryPolicy.getForkedHttpRetrier(). It's the same as the `retrier` struct
97// except it deals with the forked version of the http package.
98type forkedHttpRetrier struct {
99 *retryHelper
100
101 // The client used to perform requests.
102 client *forkedHttp.Client
103}
104
105func (ret *forkedHttpRetrier) RetryRequest(request *forkedHttp.Request) (*forkedHttp.Response, error) {
106 for {
107 response, err := ret.client.Do(request)
108 if err != nil {
109 return nil, err
110 }
111 if !ret.shouldRetry(response.StatusCode) {
112 return response, nil
113 }
114 time.Sleep(ret.policy.Delay)
115 }
116}
117
118// getRetrier returns a `retrier` object used to enforce the retry policy.
119func (policy RetryPolicy) getForkedHttpRetrier(client *forkedHttp.Client) *forkedHttpRetrier {
120 helper := policy.getRetryHelper()
121 return &forkedHttpRetrier{retryHelper: helper, client: client}
122}
0123
=== added file 'retry_policy_test.go'
--- retry_policy_test.go 1970-01-01 00:00:00 +0000
+++ retry_policy_test.go 2013-08-09 15:00:01 +0000
@@ -0,0 +1,150 @@
1// Copyright 2013 Canonical Ltd. This software is licensed under the
2// GNU Lesser General Public License version 3 (see the file COPYING).
3
4package gwacl
5
6import (
7 "fmt"
8 . "launchpad.net/gocheck"
9 forkedHttp "launchpad.net/gwacl/fork/http"
10 "net/http"
11 "time"
12)
13
14type retryPolicySuite struct{}
15
16var _ = Suite(&retryPolicySuite{})
17
18func (*retryPolicySuite) TestNoRetryPolicyDoesNotRetry(c *C) {
19 c.Check(NoRetryPolicy.NbRetries, Equals, 0)
20}
21
22func (*retryPolicySuite) TestDefaultPolicyIsNoRetryPolicy(c *C) {
23 c.Check(NoRetryPolicy, DeepEquals, RetryPolicy{})
24}
25
26func (*retryPolicySuite) TestIsRetryCodeChecksStatusCode(c *C) {
27 c.Check(
28 RetryPolicy{HttpStatusCodes: []int{http.StatusConflict}}.isRetryCode(http.StatusConflict),
29 Equals, true)
30 c.Check(
31 RetryPolicy{HttpStatusCodes: []int{}}.isRetryCode(http.StatusOK),
32 Equals, false)
33 c.Check(
34 RetryPolicy{HttpStatusCodes: []int{http.StatusConflict}}.isRetryCode(http.StatusOK),
35 Equals, false)
36
37}
38
39func (*retryPolicySuite) TestGetRetryHelperReturnsHelper(c *C) {
40 policy := RetryPolicy{NbRetries: 7, HttpStatusCodes: []int{http.StatusConflict}, Delay: time.Minute}
41 helper := policy.getRetryHelper()
42 c.Check(*helper.policy, DeepEquals, policy)
43 c.Check(helper.retriesLeft, Equals, policy.NbRetries)
44}
45
46type retryHelperSuite struct{}
47
48var _ = Suite(&retryHelperSuite{})
49
50func (*retryHelperSuite) TestShouldRetryExhaustsRetries(c *C) {
51 nbTries := 3
52 policy := RetryPolicy{NbRetries: nbTries, HttpStatusCodes: []int{http.StatusConflict}, Delay: time.Nanosecond}
53 helper := policy.getRetryHelper()
54 retries := []bool{}
55 for i := 0; i < nbTries+1; i++ {
56 retries = append(retries, helper.shouldRetry(http.StatusConflict))
57 }
58 expectedRetries := []bool{true, true, true, false}
59 c.Check(retries, DeepEquals, expectedRetries)
60}
61
62func (*retryHelperSuite) TestShouldRetryReturnsFalseIfCodeNotInHttpStatusCodes(c *C) {
63 policy := RetryPolicy{NbRetries: 10, HttpStatusCodes: []int{http.StatusConflict}, Delay: time.Nanosecond}
64 helper := policy.getRetryHelper()
65 c.Check(helper.shouldRetry(http.StatusOK), Equals, false)
66}
67
68type retrierSuite struct{}
69
70var _ = Suite(&retrierSuite{})
71
72func (*retrierSuite) TestGetRetrier(c *C) {
73 client := &http.Client{}
74 policy := RetryPolicy{NbRetries: 10, HttpStatusCodes: []int{http.StatusConflict}, Delay: time.Nanosecond}
75 retrier := policy.getRetrier(client)
76 c.Check(*retrier.policy, DeepEquals, policy)
77 c.Check(retrier.client, DeepEquals, client)
78}
79
80func (*retrierSuite) TestRetryRequest(c *C) {
81 nbTries := 3
82 transport := &MockingTransport{}
83 client := &http.Client{Transport: transport}
84 for i := 0; i < nbTries; i++ {
85 response := makeHttpResponse(http.StatusConflict, "")
86 transport.AddExchange(response, nil)
87 }
88 response := makeHttpResponse(http.StatusOK, "")
89 transport.AddExchange(response, nil)
90
91 policy := RetryPolicy{NbRetries: nbTries, HttpStatusCodes: []int{http.StatusConflict}, Delay: time.Nanosecond}
92 retrier := policy.getRetrier(client)
93 req, err := http.NewRequest("GET", "http://example.com/", nil)
94 c.Assert(err, IsNil)
95
96 resp, err := retrier.RetryRequest(req)
97 c.Assert(err, IsNil)
98
99 c.Check(resp.StatusCode, Equals, http.StatusOK)
100 c.Check(transport.ExchangeCount, Equals, nbTries+1)
101}
102
103func (*retrierSuite) TestRetryRequestBailsOutWhenError(c *C) {
104 nbTries := 3
105 transport := &MockingTransport{}
106 client := &http.Client{Transport: transport}
107 transport.AddExchange(nil, fmt.Errorf("request error"))
108
109 policy := RetryPolicy{NbRetries: nbTries, HttpStatusCodes: []int{http.StatusConflict}, Delay: time.Nanosecond}
110 retrier := policy.getRetrier(client)
111 req, err := http.NewRequest("GET", "http://example.com/", nil)
112 c.Assert(err, IsNil)
113
114 _, err = retrier.RetryRequest(req)
115 c.Check(err, ErrorMatches, ".*request error.*")
116
117 c.Check(transport.ExchangeCount, Equals, 1)
118}
119
120type forkedHttpRetrierSuite struct{}
121
122var _ = Suite(&forkedHttpRetrierSuite{})
123
124func (*forkedHttpRetrierSuite) TestGetRetrier(c *C) {
125 client := &forkedHttp.Client{}
126 policy := RetryPolicy{NbRetries: 10, HttpStatusCodes: []int{forkedHttp.StatusConflict}, Delay: time.Nanosecond}
127 retrier := policy.getForkedHttpRetrier(client)
128 c.Check(*retrier.policy, DeepEquals, policy)
129 c.Check(retrier.client, DeepEquals, client)
130}
131
132func (*forkedHttpRetrierSuite) TestRetryRequest(c *C) {
133 nbTries := 3
134 nbRequests := nbTries + 1
135 client := &forkedHttp.Client{}
136 httpRequests := make(chan *Request, nbRequests)
137 server := makeRecordingHTTPServer(httpRequests, http.StatusConflict, nil, nil)
138 defer server.Close()
139
140 policy := RetryPolicy{NbRetries: nbTries, HttpStatusCodes: []int{forkedHttp.StatusConflict}, Delay: time.Nanosecond}
141 retrier := policy.getForkedHttpRetrier(client)
142 req, err := forkedHttp.NewRequest("GET", server.URL, nil)
143 c.Assert(err, IsNil)
144
145 resp, err := retrier.RetryRequest(req)
146 c.Assert(err, IsNil)
147
148 c.Check(resp.StatusCode, Equals, forkedHttp.StatusConflict)
149 c.Check(len(httpRequests), Equals, nbTries+1)
150}
0151
=== modified file 'storage_base.go'
--- storage_base.go 2013-08-07 02:13:25 +0000
+++ storage_base.go 2013-08-09 15:00:01 +0000
@@ -214,6 +214,8 @@
214 AzureEndpoint APIEndpoint214 AzureEndpoint APIEndpoint
215215
216 client *http.Client216 client *http.Client
217
218 RetryPolicy RetryPolicy
217}219}
218220
219// getClient is used when sending a request. If a custom client is specified221// getClient is used when sending a request. If a custom client is specified
@@ -295,7 +297,9 @@
295// then an HTTPError will be returned as the error.297// then an HTTPError will be returned as the error.
296func (context *StorageContext) send(req *http.Request, res Deserializer, expectedStatus HTTPStatus) ([]byte, http.Header, error) {298func (context *StorageContext) send(req *http.Request, res Deserializer, expectedStatus HTTPStatus) ([]byte, http.Header, error) {
297 client := context.getClient()299 client := context.getClient()
298 resp, err := client.Do(req)300
301 retrier := context.RetryPolicy.getRetrier(client)
302 resp, err := retrier.RetryRequest(req)
299 if err != nil {303 if err != nil {
300 return nil, nil, err304 return nil, nil, err
301 }305 }
302306
=== modified file 'storage_base_test.go'
--- storage_base_test.go 2013-08-07 02:13:25 +0000
+++ storage_base_test.go 2013-08-09 15:00:01 +0000
@@ -67,6 +67,28 @@
67 c.Check(observed, Equals, expected)67 c.Check(observed, Equals, expected)
68}68}
6969
70type TestRetryRequests struct{}
71
72var _ = Suite(&TestRetryRequests{})
73
74func (suite *TestRetryRequests) TestRequestIsRetried(c *C) {
75 transport := &MockingTransport{}
76 body := []byte("data")
77 transport.AddExchange(&http.Response{StatusCode: http.StatusConflict, Body: Empty}, nil)
78 transport.AddExchange(&http.Response{StatusCode: http.StatusConflict, Body: Empty}, nil)
79 transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: makeResponseBody(string(body))}, nil)
80 retryPolicy := RetryPolicy{NbRetries: 3, HttpStatusCodes: []int{http.StatusConflict}, Delay: time.Nanosecond}
81 context := makeStorageContext(transport)
82 context.RetryPolicy = retryPolicy
83 req, err := http.NewRequest("GET", "http://example.com", nil)
84 c.Assert(err, IsNil)
85
86 resBody, _, err := context.send(req, nil, http.StatusOK)
87 c.Assert(err, IsNil)
88 c.Assert(transport.ExchangeCount, Equals, 3)
89 c.Check(resBody, DeepEquals, body)
90}
91
70type TestComposeCanonicalizedResource struct{}92type TestComposeCanonicalizedResource struct{}
7193
72var _ = Suite(&TestComposeCanonicalizedResource{})94var _ = Suite(&TestComposeCanonicalizedResource{})
7395
=== modified file 'x509dispatcher.go'
--- x509dispatcher.go 2013-07-30 06:49:18 +0000
+++ x509dispatcher.go 2013-08-09 15:00:01 +0000
@@ -1,3 +1,6 @@
1// Copyright 2013 Canonical Ltd. This software is licensed under the
2// GNU Lesser General Public License version 3 (see the file COPYING).
3
1package gwacl4package gwacl
25
3import (6import (
@@ -66,6 +69,8 @@
66}69}
6770
68func performX509Request(session *x509Session, request *X509Request) (*x509Response, error) {71func performX509Request(session *x509Session, request *X509Request) (*x509Response, error) {
72 response := &x509Response{}
73
69 Debugf("Request: %s %s", request.Method, request.URL)74 Debugf("Request: %s %s", request.Method, request.URL)
70 if len(request.Payload) > 0 {75 if len(request.Payload) > 0 {
71 Debugf("Request body:\n%s", request.Payload)76 Debugf("Request body:\n%s", request.Payload)
@@ -78,15 +83,12 @@
78 }83 }
79 httpRequest.Header.Set("Content-Type", request.ContentType)84 httpRequest.Header.Set("Content-Type", request.ContentType)
80 httpRequest.Header.Set("x-ms-version", request.APIVersion)85 httpRequest.Header.Set("x-ms-version", request.APIVersion)
81 httpResponse, err := session.client.Do(httpRequest)86 retrier := session.retryPolicy.getForkedHttpRetrier(session.client)
87 httpResponse, err := retrier.RetryRequest(httpRequest)
82 if err != nil {88 if err != nil {
83 return nil, err89 return nil, err
84 }90 }
85 if httpResponse.Body != nil {
86 defer httpResponse.Body.Close()
87 }
8891
89 response := &x509Response{}
90 response.StatusCode = httpResponse.StatusCode92 response.StatusCode = httpResponse.StatusCode
91 response.Body, err = readAndClose(httpResponse.Body)93 response.Body, err = readAndClose(httpResponse.Body)
92 if err != nil {94 if err != nil {
9395
=== modified file 'x509dispatcher_test.go'
--- x509dispatcher_test.go 2013-08-06 05:11:29 +0000
+++ x509dispatcher_test.go 2013-08-09 15:00:01 +0000
@@ -5,6 +5,7 @@
5 . "launchpad.net/gocheck"5 . "launchpad.net/gocheck"
6 "net/http"6 "net/http"
7 "net/http/httptest"7 "net/http/httptest"
8 "time"
8)9)
910
10type x509DispatcherSuite struct{}11type x509DispatcherSuite struct{}
@@ -49,7 +50,7 @@
49 server := makeRecordingHTTPServer(httpRequests, http.StatusOK, nil, nil)50 server := makeRecordingHTTPServer(httpRequests, http.StatusOK, nil, nil)
50 defer server.Close()51 defer server.Close()
51 // No real certificate needed since we're testing on http, not https.52 // No real certificate needed since we're testing on http, not https.
52 session, err := newX509Session("subscriptionid", "", "West US")53 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
53 c.Assert(err, IsNil)54 c.Assert(err, IsNil)
54 path := "/foo/bar"55 path := "/foo/bar"
55 version := "test-version"56 version := "test-version"
@@ -66,6 +67,28 @@
66 c.Check(httpRequest.BodyContent, HasLen, 0)67 c.Check(httpRequest.BodyContent, HasLen, 0)
67}68}
6869
70func (*x509DispatcherSuite) TestRetryPolicyCausesRequestsToBeRetried(c *C) {
71 nbRetries := 2
72 nbRequests := nbRetries + 1
73 httpRequests := make(chan *Request, nbRequests)
74 server := makeRecordingHTTPServer(httpRequests, http.StatusConflict, nil, nil)
75 defer server.Close()
76 // No real certificate needed since we're testing on http, not https.
77 retryPolicy := RetryPolicy{NbRetries: nbRetries, HttpStatusCodes: []int{http.StatusConflict}, Delay: time.Nanosecond}
78 session, err := newX509Session("subscriptionid", "", "West US", retryPolicy)
79 c.Assert(err, IsNil)
80 path := "/foo/bar"
81 version := "test-version"
82 request := newX509RequestGET(server.URL+path, version)
83
84 response, err := performX509Request(session, request)
85 c.Assert(err, IsNil)
86 c.Assert(response.StatusCode, Equals, http.StatusConflict)
87
88 // nbRequests request were performed.
89 c.Check(httpRequests, HasLen, nbRequests)
90}
91
69func (*x509DispatcherSuite) TestPostRequestDoesHTTPPOST(c *C) {92func (*x509DispatcherSuite) TestPostRequestDoesHTTPPOST(c *C) {
70 httpRequests := make(chan *Request, 1)93 httpRequests := make(chan *Request, 1)
71 requestBody := []byte{1, 2, 3}94 requestBody := []byte{1, 2, 3}
@@ -74,7 +97,7 @@
74 server := makeRecordingHTTPServer(httpRequests, http.StatusOK, responseBody, nil)97 server := makeRecordingHTTPServer(httpRequests, http.StatusOK, responseBody, nil)
75 defer server.Close()98 defer server.Close()
76 // No real certificate needed since we're testing on http, not https.99 // No real certificate needed since we're testing on http, not https.
77 session, err := newX509Session("subscriptionid", "", "West US")100 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
78 c.Assert(err, IsNil)101 c.Assert(err, IsNil)
79 path := "/foo/bar"102 path := "/foo/bar"
80 version := "test-version"103 version := "test-version"
@@ -98,7 +121,7 @@
98 server := makeRecordingHTTPServer(httpRequests, http.StatusOK, nil, nil)121 server := makeRecordingHTTPServer(httpRequests, http.StatusOK, nil, nil)
99 defer server.Close()122 defer server.Close()
100 // No real certificate needed since we're testing on http, not https.123 // No real certificate needed since we're testing on http, not https.
101 session, err := newX509Session("subscriptionid", "", "West US")124 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
102 c.Assert(err, IsNil)125 c.Assert(err, IsNil)
103 path := "/foo/bar"126 path := "/foo/bar"
104 version := "test-version"127 version := "test-version"
@@ -122,7 +145,7 @@
122 server := makeRecordingHTTPServer(httpRequests, http.StatusOK, responseBody, nil)145 server := makeRecordingHTTPServer(httpRequests, http.StatusOK, responseBody, nil)
123 defer server.Close()146 defer server.Close()
124 // No real certificate needed since we're testing on http, not https.147 // No real certificate needed since we're testing on http, not https.
125 session, err := newX509Session("subscriptionid", "", "West US")148 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
126 c.Assert(err, IsNil)149 c.Assert(err, IsNil)
127 path := "/foo/bar"150 path := "/foo/bar"
128 version := "test-version"151 version := "test-version"
@@ -151,7 +174,7 @@
151 serveMux.HandleFunc("/", returnRequest)174 serveMux.HandleFunc("/", returnRequest)
152 server := httptest.NewServer(serveMux)175 server := httptest.NewServer(serveMux)
153 defer server.Close()176 defer server.Close()
154 session, err := newX509Session("subscriptionid", "", "West US")177 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
155 c.Assert(err, IsNil)178 c.Assert(err, IsNil)
156 path := "/foo/bar"179 path := "/foo/bar"
157 request := newX509RequestGET(server.URL+path, "testversion")180 request := newX509RequestGET(server.URL+path, "testversion")
158181
=== modified file 'x509session.go'
--- x509session.go 2013-08-06 05:31:49 +0000
+++ x509session.go 2013-08-09 15:00:01 +0000
@@ -16,13 +16,14 @@
16 certFile string16 certFile string
17 client *http.Client17 client *http.Client
18 baseURL *url.URL18 baseURL *url.URL
19 retryPolicy RetryPolicy
19}20}
2021
21// newX509Session creates and returns a new x509Session based on credentials22// newX509Session creates and returns a new x509Session based on credentials
22// and X509 certificate files.23// and X509 certificate files.
23// For testing purposes, certFile can be passed as the empty string and it24// For testing purposes, certFile can be passed as the empty string and it
24// will be ignored.25// will be ignored.
25func newX509Session(subscriptionId, certFile, location string) (*x509Session, error) {26func newX509Session(subscriptionId, certFile, location string, retryPolicy RetryPolicy) (*x509Session, error) {
26 certs := []tls.Certificate{}27 certs := []tls.Certificate{}
27 if certFile != "" {28 if certFile != "" {
28 //29 //
@@ -51,6 +52,7 @@
51 certFile: certFile,52 certFile: certFile,
52 client: &client,53 client: &client,
53 baseURL: baseURL,54 baseURL: baseURL,
55 retryPolicy: retryPolicy,
54 }56 }
55 return &session, nil57 return &session, nil
56}58}
5759
=== modified file 'x509session_test.go'
--- x509session_test.go 2013-08-06 05:31:49 +0000
+++ x509session_test.go 2013-08-09 15:00:01 +0000
@@ -109,7 +109,7 @@
109}109}
110110
111func (suite *x509SessionSuite) TestNewX509Session(c *C) {111func (suite *x509SessionSuite) TestNewX509Session(c *C) {
112 session, err := newX509Session("subscriptionid", "", "China East")112 session, err := newX509Session("subscriptionid", "", "China East", NoRetryPolicy)
113 c.Assert(err, IsNil)113 c.Assert(err, IsNil)
114 c.Assert(session.baseURL, NotNil)114 c.Assert(session.baseURL, NotNil)
115 c.Check(session.baseURL.String(), Equals, GetEndpoint("China East").ManagementAPI())115 c.Check(session.baseURL.String(), Equals, GetEndpoint("China East").ManagementAPI())
@@ -118,7 +118,7 @@
118func (suite *x509SessionSuite) TestComposeURLComposesURLWithRelativePath(c *C) {118func (suite *x509SessionSuite) TestComposeURLComposesURLWithRelativePath(c *C) {
119 const subscriptionID = "subscriptionid"119 const subscriptionID = "subscriptionid"
120 const path = "foo/bar"120 const path = "foo/bar"
121 session, err := newX509Session(subscriptionID, "", "West US")121 session, err := newX509Session(subscriptionID, "", "West US", NoRetryPolicy)
122 c.Assert(err, IsNil)122 c.Assert(err, IsNil)
123123
124 url := session.composeURL(path)124 url := session.composeURL(path)
@@ -132,7 +132,7 @@
132 c.Assert(err, NotNil)132 c.Assert(err, NotNil)
133 c.Check(err, ErrorMatches, ".*absolute.*path.*")133 c.Check(err, ErrorMatches, ".*absolute.*path.*")
134 }()134 }()
135 session, err := newX509Session("subscriptionid", "", "West US")135 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
136 c.Assert(err, IsNil)136 c.Assert(err, IsNil)
137137
138 // This panics because we're passing an absolute path.138 // This panics because we're passing an absolute path.
@@ -142,7 +142,7 @@
142func (suite *x509SessionSuite) TestGetServerErrorProducesServerError(c *C) {142func (suite *x509SessionSuite) TestGetServerErrorProducesServerError(c *C) {
143 msg := "huhwhat"143 msg := "huhwhat"
144 status := http.StatusNotFound144 status := http.StatusNotFound
145 session, err := newX509Session("subscriptionid", "", "West US")145 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
146 c.Assert(err, IsNil)146 c.Assert(err, IsNil)
147147
148 err = session.getServerError(status, []byte{}, msg)148 err = session.getServerError(status, []byte{}, msg)
@@ -158,7 +158,7 @@
158 http.StatusOK,158 http.StatusOK,
159 http.StatusNoContent,159 http.StatusNoContent,
160 }160 }
161 session, err := newX509Session("subscriptionid", "", "West US")161 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
162 c.Assert(err, IsNil)162 c.Assert(err, IsNil)
163163
164 for _, status := range goodCodes {164 for _, status := range goodCodes {
@@ -176,7 +176,7 @@
176 http.StatusInternalServerError,176 http.StatusInternalServerError,
177 http.StatusNotImplemented,177 http.StatusNotImplemented,
178 }178 }
179 session, err := newX509Session("subscriptionid", "", "West US")179 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
180 c.Assert(err, IsNil)180 c.Assert(err, IsNil)
181181
182 for _, status := range badCodes {182 for _, status := range badCodes {
@@ -187,7 +187,7 @@
187func (suite *x509SessionSuite) TestGetIssuesRequest(c *C) {187func (suite *x509SessionSuite) TestGetIssuesRequest(c *C) {
188 subscriptionID := "subscriptionID"188 subscriptionID := "subscriptionID"
189 uri := "resource"189 uri := "resource"
190 session, err := newX509Session(subscriptionID, "", "West US")190 session, err := newX509Session(subscriptionID, "", "West US", NoRetryPolicy)
191 c.Assert(err, IsNil)191 c.Assert(err, IsNil)
192 // Record incoming requests, and have them return a given reply.192 // Record incoming requests, and have them return a given reply.
193 fixedResponse := x509Response{193 fixedResponse := x509Response{
@@ -211,7 +211,7 @@
211}211}
212212
213func (suite *x509SessionSuite) TestGetReportsClientSideError(c *C) {213func (suite *x509SessionSuite) TestGetReportsClientSideError(c *C) {
214 session, err := newX509Session("subscriptionid", "", "West US")214 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
215 msg := "could not dispatch request"215 msg := "could not dispatch request"
216 rigFailingDispatcher(fmt.Errorf(msg))216 rigFailingDispatcher(fmt.Errorf(msg))
217217
@@ -223,7 +223,7 @@
223}223}
224224
225func (suite *x509SessionSuite) TestGetReportsServerSideError(c *C) {225func (suite *x509SessionSuite) TestGetReportsServerSideError(c *C) {
226 session, err := newX509Session("subscriptionid", "", "West US")226 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
227 fixedResponse := x509Response{227 fixedResponse := x509Response{
228 StatusCode: http.StatusForbidden,228 StatusCode: http.StatusForbidden,
229 Body: []byte("Body"),229 Body: []byte("Body"),
@@ -244,7 +244,7 @@
244 version := "test-version"244 version := "test-version"
245 requestBody := []byte("Request body")245 requestBody := []byte("Request body")
246 requestContentType := "bogusContentType"246 requestContentType := "bogusContentType"
247 session, err := newX509Session(subscriptionID, "", "West US")247 session, err := newX509Session(subscriptionID, "", "West US", NoRetryPolicy)
248 c.Assert(err, IsNil)248 c.Assert(err, IsNil)
249 // Record incoming requests, and have them return a given reply.249 // Record incoming requests, and have them return a given reply.
250 fixedResponse := x509Response{250 fixedResponse := x509Response{
@@ -269,7 +269,7 @@
269}269}
270270
271func (suite *x509SessionSuite) TestPostReportsClientSideError(c *C) {271func (suite *x509SessionSuite) TestPostReportsClientSideError(c *C) {
272 session, err := newX509Session("subscriptionid", "", "West US")272 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
273 msg := "could not dispatch request"273 msg := "could not dispatch request"
274 rigFailingDispatcher(fmt.Errorf(msg))274 rigFailingDispatcher(fmt.Errorf(msg))
275275
@@ -281,7 +281,7 @@
281}281}
282282
283func (suite *x509SessionSuite) TestPostReportsServerSideError(c *C) {283func (suite *x509SessionSuite) TestPostReportsServerSideError(c *C) {
284 session, err := newX509Session("subscriptionid", "", "West US")284 session, err := newX509Session("subscriptionid", "", "West US", NoRetryPolicy)
285 fixedResponse := x509Response{285 fixedResponse := x509Response{
286 StatusCode: http.StatusForbidden,286 StatusCode: http.StatusForbidden,
287 Body: []byte("Body"),287 Body: []byte("Body"),
@@ -300,7 +300,7 @@
300 subscriptionID := "subscriptionID"300 subscriptionID := "subscriptionID"
301 uri := "resource"301 uri := "resource"
302 version := "test-version"302 version := "test-version"
303 session, err := newX509Session(subscriptionID, "", "West US")303 session, err := newX509Session(subscriptionID, "", "West US", NoRetryPolicy)
304 c.Assert(err, IsNil)304 c.Assert(err, IsNil)
305 // Record incoming requests, and have them return a given reply.305 // Record incoming requests, and have them return a given reply.
306 fixedResponse := x509Response{StatusCode: http.StatusOK}306 fixedResponse := x509Response{StatusCode: http.StatusOK}
@@ -324,7 +324,7 @@
324 uri := "resource"324 uri := "resource"
325 version := "test-version"325 version := "test-version"
326 requestBody := []byte("Request body")326 requestBody := []byte("Request body")
327 session, err := newX509Session(subscriptionID, "", "West US")327 session, err := newX509Session(subscriptionID, "", "West US", NoRetryPolicy)
328 c.Assert(err, IsNil)328 c.Assert(err, IsNil)
329 // Record incoming requests, and have them return a given reply.329 // Record incoming requests, and have them return a given reply.
330 fixedResponse := x509Response{330 fixedResponse := x509Response{

Subscribers

People subscribed via source and target branches

to all changes: