Merge lp:~jtv/gwacl/poll-if-needed into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 86
Merged at revision: 84
Proposed branch: lp:~jtv/gwacl/poll-if-needed
Merge into: lp:gwacl
Diff against target: 313 lines (+160/-28)
6 files modified
httperror.go (+16/-1)
httperror_test.go (+28/-0)
managementapi.go (+32/-19)
managementapi_test.go (+77/-5)
poller.go (+2/-3)
xmlobjects.go (+5/-0)
To merge this branch: bzr merge lp:~jtv/gwacl/poll-if-needed
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+156341@code.launchpad.net

Commit message

Support both synchronous and asynchronous operations in blockUntilCompleted().

Description of the change

With this change in place, we'll be able to call blockUntilCompleted() for every request that might possibly be asynchronous — without caring whether it actually is.

This will make us more robust with regard to changes in which operations are asynchronous, or inconsistencies between documentation and the real Azure service. All POST/PUT/DELETE requests can simply return blockUntilCompleted(response) and they will work regardless of how Azure chooses to process them.

Jeroen

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

Excellent change, thanks. TestBlockUntilCompletedSucceedsOnSyncSuccess was really hard to read and understand but I don't have any constructive things to say about it, maybe this is just How It Has To Be with Go...

49 +func (suite *httpErrorSuite) TestNewAzureErrorFromOperation(c *C) {

Again I think consts are not useful (you may disagree) but use DeepEquals with a single struct check here.

91 +// blockUntilCompleted blocks if necessary. It does not return until the
92 +// requested operation is finished (whether successfully or unsuccessfully).
93 +// Pass in the response from the request that you want to block on. If the
94 +// response indicates that the request finished synchronously (or failed in
95 +// whatever way) then this will not block. However, if the response indicates
96 +// that the Azure service is handling the operation asynchronously, it will
97 +// poll until completion.

I found this hard to read/parse. Can I suggest a replacement:

// blockUntilCompleted blocks as necessary. It does not return until the
// requested operation has finished (whether successfully or unsuccessfully).
// The passed 'response' argument is checked and the following actions will
// occur:
// * response is synchronous: return immediately
// * response is a failure: return immediately
// * response is asynchronous: poll until the operation completes, then return

118 + case http.StatusOK, http.StatusCreated, http.StatusNoContent:

Are you sure there's no other successful statuses? In the storage stuff we checked to see if it was 200-299. Honestly, I'm not sure which is better though!

189 + c.Check(err, NotNil)

Use Assert not Check here, there's not much point continuing if this bit fails.

review: Approve
lp:~jtv/gwacl/poll-if-needed updated
84. By Jeroen T. Vermeulen

Review changes.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> Excellent change, thanks. TestBlockUntilCompletedSucceedsOnSyncSuccess was
> really hard to read and understand but I don't have any constructive things to
> say about it, maybe this is just How It Has To Be with Go...

And that was one of the shortest tests! I made some changes and also applied them to the longer equivalent, TestBlockUntilCompletedReturnsHTTPErrorOnSyncFailure. Should produce better error output, too.

> 49 +func (suite *httpErrorSuite) TestNewAzureErrorFromOperation(c *C) {
>
> Again I think consts are not useful (you may disagree) but use DeepEquals with
> a single struct check here.

Turns out I can't do that. There's an “error” in there which contains a pointer to an error that has been created on the fly. DeepEquals will compare the pointers instead of what they point to, and fail.

> 91 +// blockUntilCompleted blocks if necessary. It does not return until
> the
> 92 +// requested operation is finished (whether successfully or
> unsuccessfully).
> 93 +// Pass in the response from the request that you want to block on.
> If the
> 94 +// response indicates that the request finished synchronously (or
> failed in
> 95 +// whatever way) then this will not block. However, if the response
> indicates
> 96 +// that the Azure service is handling the operation asynchronously,
> it will
> 97 +// poll until completion.
>
> I found this hard to read/parse. Can I suggest a replacement:

Rewritten. I avoided enumerating the cases in quite that detail, but I think it's easier to read now.

> 118 + case http.StatusOK, http.StatusCreated, http.StatusNoContent:
>
> Are you sure there's no other successful statuses? In the storage stuff we
> checked to see if it was 200-299. Honestly, I'm not sure which is better
> though!

On the contrary: I know there are other successful statuses. One of them is the very “Accepted” which tells us that an operation is asynchronous. Another one is “Partial Response.” The important thing is that these require special handling that we do not have, and so it's actually better to return an error than to proceed with possibly invalid data.

I went by RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

An API might also return “Found” after creating an object on request, but that's a redirect and I don't think the client would bother exposing it to us as a separate response. Or if it does, all our existing code would expose it as an error as well. It'd be better to fail and know than to risk misinterpreting failures.

> 189 + c.Check(err, NotNil)
>
> Use Assert not Check here, there's not much point continuing if this bit
> fails.

In this case, there is. A failure here shows that one particular status is not being handled correctly. It's just that you get two failures for every nil, and the failures don't state what status code triggers the problem.

This example shows that the c.Check()/c.Assert() distinction in gocheck, even though it addresses a real problem, is not all that helpful. I rewrote the checks using a map of expected results.

lp:~jtv/gwacl/poll-if-needed updated
85. By Jeroen T. Vermeulen

Review change: rewrote function documentation.

86. By Jeroen T. Vermeulen

Review change: some small test simplifications, and clearer test output.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'httperror.go'
2--- httperror.go 2013-03-27 16:34:04 +0000
3+++ httperror.go 2013-04-02 07:42:21 +0000
4@@ -58,7 +58,7 @@
5 return fmt.Sprintf("%s (%d: %s)", description, status, name)
6 }
7
8-// NewHTTPError returns the appropriate HTTPError implementation for a given
9+// newHTTPError returns the appropriate HTTPError implementation for a given
10 // HTTP response.
11 // It takes a status code and response body, rather than just a standard
12 // http.Response object, because it must work with go-curl as well as Go's
13@@ -75,3 +75,18 @@
14 }
15 return &azureError
16 }
17+
18+// newAzureErrorFromOperation composes an HTTPError based on an Operation
19+// struct, i.e. the result of an asynchronous operation.
20+func newAzureErrorFromOperation(outcome *Operation) *AzureError {
21+ if outcome.Status != FailedOperationStatus {
22+ msg := fmt.Errorf("interpreting Azure %s as an asynchronous failure", outcome.Status)
23+ panic(msg)
24+ }
25+ return &AzureError{
26+ error: errors.New("asynchronous operation failed"),
27+ HTTPStatus: HTTPStatus(outcome.HttpStatusCode),
28+ Code: outcome.ErrorCode,
29+ Message: outcome.ErrorMessage,
30+ }
31+}
32
33=== modified file 'httperror_test.go'
34--- httperror_test.go 2013-03-27 16:34:04 +0000
35+++ httperror_test.go 2013-04-02 07:42:21 +0000
36@@ -4,6 +4,7 @@
37 "errors"
38 "fmt"
39 . "launchpad.net/gocheck"
40+ "net/http"
41 )
42
43 type httpErrorSuite struct{}
44@@ -66,3 +67,30 @@
45 }
46 c.Check(httpErr.Error(), Equals, "something failed (501: Not Implemented)")
47 }
48+
49+func (suite *httpErrorSuite) TestNewAzureErrorFromOperation(c *C) {
50+ status := http.StatusConflict
51+ code := MakeRandomString(7)
52+ message := MakeRandomString(20)
53+ // Test body copied from Azure documentation, mutatis mutandi.
54+ body := fmt.Sprintf(`
55+ <?xml version="1.0" encoding="utf-8"?>
56+ <Operation xmlns="http://schemas.microsoft.com/windowsazure">
57+ <ID>%s</ID>
58+ <Status>Failed</Status>
59+ <HttpStatusCode>%d</HttpStatusCode>
60+ <Error>
61+ <Code>%s</Code>
62+ <Message>%s</Message>
63+ </Error>
64+ </Operation>
65+ `,
66+ MakeRandomString(5), status, code, message)
67+ operation := Operation{}
68+ operation.Deserialize([]byte(body))
69+
70+ err := newAzureErrorFromOperation(&operation)
71+ c.Check(err.HTTPStatus, Equals, HTTPStatus(status))
72+ c.Check(err.Code, Equals, code)
73+ c.Check(err.Message, Equals, message)
74+}
75
76=== modified file 'managementapi.go'
77--- managementapi.go 2013-04-02 03:44:55 +0000
78+++ managementapi.go 2013-04-02 07:42:21 +0000
79@@ -41,26 +41,39 @@
80 // Set this to 0 to prevent polling from happening.
81 var pollerInterval = 10 * time.Second
82
83-// blockUntilCompleted blocks until the operation referenced in the headers of the given response
84-// is completed. It returns an error if the given response does not contain
85-// a reference to an asychronous operation, if the response from the server
86-// cannot be parsed, or if the asynchronous operation fails.
87-// Internally, it polls the server at regular intervals until the operation is completed.
88+// blockUntilCompleted blocks and polls for completion of an Azure operation.
89+// The "response" parameter is the result of the request that started the
90+// operation. If the response says that the operation is running
91+// asynchronously, this function will block and poll until the operation is
92+// finished. On the other hand, if the response was a failure or a synchronous
93+// result, the function returns immediately.
94 func (api *ManagementAPI) blockUntilCompleted(response *x509Response) error {
95- if pollerInterval != 0 {
96- operationID, err := getOperationID(response)
97- if err != nil {
98- return err
99- }
100- poller := newOperationPoller(api, operationID)
101- operation, err := performOperationPolling(poller, pollerInterval)
102- if err != nil {
103- return err
104- }
105- if operation.Status != SucceededOperationStatus {
106- err := fmt.Errorf("asychronous operation failed (status: %v)", operation.Status)
107- return err
108- }
109+ switch response.StatusCode {
110+ case http.StatusAccepted:
111+ // Asynchronous. Fall out of the switch and start blocking.
112+ case http.StatusOK, http.StatusCreated, http.StatusNoContent:
113+ // Simple success. Sometimes it happens; enjoy it.
114+ return nil
115+ default:
116+ // Request failed, synchronously.
117+ return newHTTPError(response.StatusCode, response.Body, "request failed")
118+ }
119+
120+ if pollerInterval == 0 {
121+ // Polling has been disabled for test purposes. Return immediately.
122+ return nil
123+ }
124+ operationID, err := getOperationID(response)
125+ if err != nil {
126+ return fmt.Errorf("could not interpret asynchronous response: %v", err)
127+ }
128+ poller := newOperationPoller(api, operationID)
129+ operation, err := performOperationPolling(poller, pollerInterval)
130+ if err != nil {
131+ return err
132+ }
133+ if operation.Status != SucceededOperationStatus {
134+ return newAzureErrorFromOperation(operation)
135 }
136 return nil
137 }
138
139=== modified file 'managementapi_test.go'
140--- managementapi_test.go 2013-04-02 03:44:55 +0000
141+++ managementapi_test.go 2013-04-02 07:42:21 +0000
142@@ -5,9 +5,11 @@
143
144 import (
145 "encoding/xml"
146+ "errors"
147 "fmt"
148 . "launchpad.net/gocheck"
149 "net/http"
150+ "strings"
151 "time"
152 )
153
154@@ -56,7 +58,77 @@
155 c.Check(returnedOperationID, Equals, operationID)
156 }
157
158-func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedErrorsIfOperationFails(c *C) {
159+func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedSucceedsOnSyncSuccess(c *C) {
160+ // Set of expected error returns for success statuses.
161+ // (They all map to nil. It makes it easier further down to report
162+ // unexpected errors in a helpful way).
163+ expectedErrors := map[int]error{
164+ http.StatusOK: nil,
165+ http.StatusCreated: nil,
166+ http.StatusNoContent: nil,
167+ }
168+ api := makeAPI(c)
169+
170+ receivedErrors := make(map[int]error)
171+ for status := range expectedErrors {
172+ err := api.blockUntilCompleted(&x509Response{StatusCode: status})
173+ receivedErrors[status] = err
174+ }
175+
176+ c.Check(receivedErrors, DeepEquals, expectedErrors)
177+}
178+
179+func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedReturnsHTTPErrorOnSyncFailure(c *C) {
180+ // Set of failure statuses, and whether they're supposed to return
181+ // HTTPError.
182+ // (They all map to true. It makes it easier further down to report
183+ // failures in a helpful way).
184+ expectedErrors := map[int]bool{
185+ http.StatusBadRequest: true,
186+ http.StatusUnauthorized: true,
187+ http.StatusForbidden: true,
188+ http.StatusNotFound: true,
189+ http.StatusConflict: true,
190+ http.StatusInternalServerError: true,
191+ http.StatusNotImplemented: true,
192+ }
193+ api := makeAPI(c)
194+
195+ receivedErrors := make(map[int]bool)
196+ for status := range expectedErrors {
197+ err := api.blockUntilCompleted(&x509Response{StatusCode: status})
198+ _, ok := err.(HTTPError)
199+ receivedErrors[status] = ok
200+ }
201+
202+ c.Check(receivedErrors, DeepEquals, expectedErrors)
203+}
204+
205+func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedPollsOnAsyncOperation(c *C) {
206+ const operationID = "async-operation-id"
207+ // For this test, we can't leave polling disabled. Instead, we set it to
208+ // sleep as briefly as possible.
209+ pollerInterval = time.Nanosecond
210+ // We set the dispatcher up for failure, and prove that
211+ // blockUntilCompleted() makes a polling request.
212+ failure := errors.New("Simulated failure")
213+ rigFailingDispatcher(failure)
214+ requests := make([]*x509Request, 0)
215+ rigRecordingDispatcher(&requests)
216+ api := makeAPI(c)
217+ accepted := makeX509ResponseWithOperationHeader(operationID)
218+
219+ err := api.blockUntilCompleted(accepted)
220+
221+ c.Check(err, Equals, failure)
222+ c.Assert(len(requests), Equals, 1)
223+ requestURL := requests[0].URL
224+ urlParts := strings.Split(requestURL, "/")
225+ polledOperationID := urlParts[len(urlParts)-1]
226+ c.Check(polledOperationID, Equals, operationID)
227+}
228+
229+func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedErrorsIfAsyncOperationFails(c *C) {
230 response := DispatcherResponse{
231 response: &x509Response{
232 Body: []byte(fmt.Sprintf(operationXMLTemplate, "Failed")),
233@@ -70,10 +142,10 @@
234 api := makeAPI(c)
235
236 err := api.blockUntilCompleted(operationResponse)
237- c.Check(err, ErrorMatches, ".*asychronous operation failed.*")
238+ c.Check(err, ErrorMatches, ".*asynchronous operation failed.*")
239 }
240
241-func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedErrorsIfInvalidXML(c *C) {
242+func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedErrorsOnInvalidXML(c *C) {
243 response := DispatcherResponse{
244 response: &x509Response{
245 Body: []byte(">invalidXML<"),
246@@ -90,7 +162,7 @@
247 c.Check(err, FitsTypeOf, new(xml.SyntaxError))
248 }
249
250-func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedErrorsIfFailsToPoll(c *C) {
251+func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedErrorsIfPollingFails(c *C) {
252 response := DispatcherResponse{
253 response: &x509Response{},
254 errorObject: fmt.Errorf("unexpected error")}
255@@ -104,7 +176,7 @@
256 c.Check(err, ErrorMatches, ".*unexpected error.*")
257 }
258
259-func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedSucceedsIfOperationSucceeds(c *C) {
260+func (suite *managementAPIUtilitiesSuite) TestBlockUntilCompletedSucceedsIfAsyncOperationSucceeds(c *C) {
261 response := DispatcherResponse{
262 response: &x509Response{
263 Body: []byte(fmt.Sprintf(operationXMLTemplate, "Succeeded")),
264
265=== modified file 'poller.go'
266--- poller.go 2013-03-29 08:45:30 +0000
267+++ poller.go 2013-04-02 07:42:21 +0000
268@@ -4,7 +4,6 @@
269 package gwacl
270
271 import (
272- "encoding/xml"
273 "time"
274 )
275
276@@ -53,7 +52,7 @@
277 return nil, err
278 }
279 operation := Operation{}
280- err = xml.Unmarshal(response.Body, &operation)
281+ err = operation.Deserialize(response.Body)
282 return &operation, err
283 }
284
285@@ -90,7 +89,7 @@
286 // server cannot be reached.
287 if response.StatusCode >= 200 && response.StatusCode < 300 {
288 operation := Operation{}
289- err := xml.Unmarshal(response.Body, &operation)
290+ err := operation.Deserialize(response.Body)
291 if err != nil {
292 return false, err
293 }
294
295=== modified file 'xmlobjects.go'
296--- xmlobjects.go 2013-04-01 07:27:12 +0000
297+++ xmlobjects.go 2013-04-02 07:42:21 +0000
298@@ -528,6 +528,7 @@
299 const (
300 InProgressOperationStatus = "InProgress"
301 SucceededOperationStatus = "Succeeded"
302+ FailedOperationStatus = "Failed"
303 )
304
305 type Operation struct {
306@@ -537,3 +538,7 @@
307 ErrorCode string `xml:"Error>Code"`
308 ErrorMessage string `xml:"Error>Message"`
309 }
310+
311+func (o *Operation) Deserialize(data []byte) error {
312+ return xml.Unmarshal(data, o)
313+}

Subscribers

People subscribed via source and target branches